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/.gitignore b/.gitignore index 815e8cc827..2d9f8bd65a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,14 +2,10 @@ node_modules .cache .temp package-lock.json +yarn.lock +package.json .DS_Store dump.rdb docs/.vuepress/.cache/ docs/.vuepress/.temp/ -docs/dist/ -dist.zip -images -*.log -.yarn -*-vip.md -/.vscode +docs/.vuepress/dist/ 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..621ee47103 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,22 @@ +>[二哥的编程星球](https://tobebetterjavaer.com/zhishixingqiu/)已经有 **800 多名** 小伙伴加入了,如果你也需要一个良好的学习氛围,[戳链接](https://tobebetterjavaer.com/zhishixingqiu/)加入我们吧!这是一个 Java 学习指南 + 编程实战的私密圈子,你可以向二哥提问、帮你制定学习计划、跟着二哥一起做项目、刷力扣,冲冲冲。

- - 二哥的Java进阶之路 + + Java 程序员进阶之路

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

- Github | - Gitee + + 无套路下载 + Java程序员进阶之路

# 为什么会有这个开源知识库 -> 知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是我自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 二哥的Java进阶之路😄。 +> 知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 Java 程序员进阶之路😄。 > > 知识库旨在为学习 Java 的小伙伴提供一系列: > - **优质的原创 Java 教程** @@ -28,394 +27,351 @@ > > 赠人玫瑰手有余香。知识库会持续保持**更新**,欢迎收藏品鉴! > -> **转载须知** :以下所有文章如非文首说明为转载皆为我(沉默王二)的原创,且不允许转载,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! +> **转载须知** :以下所有文章如非文首说明为转载皆为我(沉默王二)的原创,转载在文首注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! > > 推荐你通过在线阅读网站进行阅读,体验更好,速度更快! > -> - [**二哥的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 程序员进阶之路在线阅读网站](https://tobebetterjavaer.com/) +> - [Java 程序员进阶之路 GitHub 仓库](https://github.com/itwanger/toBeBetterJavaer) +> - [Java 程序员进阶之路 Gitee 仓库](https://gitee.com/itwanger/toBeBetterJavaer) +> +> 亿点点小请求,建议戳[这个链接🔗](https://github.com/itwanger/toBeBetterJavaer)给本仓库点个 star,满足一下我的虚荣心,内容质量也绝对值得你一个 star。我还在继续创作,给我一点继续更新的动力,笔芯。 +> +> 👍推荐[免费下载1000本计算机经典书籍](https://github.com/itwanger/JavaBooks) # 知识库地图 > 知识库收录的核心内容就全在这里面了,大类分为 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) +![](https://itwanger-oss.oss-cn-beijing.aliyuncs.com/tobebetterjavaer/images/tobebetterjavaer-map.png) # 学习路线 -> 除了 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 学习路线,还有 C语言、C++、Python、Go 语言、操作系统、前端、数据结构与算法、蓝桥杯、大数据、Android、MySQL等硬核学习路线,欢迎收藏品鉴! + + * [Java学习路线一条龙版(建议收藏🔥)](docs/xuexiluxian/java/yitiaolong.md) + * [Java并发编程学习路线(建议收藏🔥)](docs/xuexiluxian/java/thread.md) + * [Java虚拟机学习路线(建议收藏🔥)](docs/xuexiluxian/java/jvm.md) + * [C语言学习路线(建议收藏🔥)](docs/xuexiluxian/c.md) + * [C++学习路线(建议收藏🔥)](docs/xuexiluxian/ccc.md) + * [Python学习路线(建议收藏🔥)](docs/xuexiluxian/python.md) + * [Go语言学习路线(建议收藏🔥)](docs/xuexiluxian/go.md) + * [操作系统学习路线(建议收藏🔥)](docs/xuexiluxian/os.md) + * [前端学习路线(建议收藏🔥)](docs/xuexiluxian/qianduan.md) + * [算法和数据结构学习路线(建议收藏🔥)](docs/xuexiluxian/algorithm.md) + * [蓝桥杯学习路线(建议收藏🔥)](docs/xuexiluxian/lanqiaobei.md) + * [大数据学习路线(建议收藏🔥)](docs/xuexiluxian/bigdata.md) + * [Android 安卓学习路线(建议收藏🔥)](docs/xuexiluxian/android.md) + * [MySQL 学习路线(建议收藏🔥)](docs/xuexiluxian/mysql.md) # 面渣逆袭 -> **面试前必读系列**!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ、分布式、微服务、设计模式、Linux 等等。 +> **面试前必读系列**!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ 等等。 + +- [面渣逆袭(Java 基础篇八股文面试题)必看👍](docs/sidebar/sanfene/javase.md) +- [面渣逆袭(Java 集合框架篇八股文面试题)必看👍](docs/sidebar/sanfene/collection.md) +- [面渣逆袭(Java 并发编程篇八股文面试题)必看👍](docs/sidebar/sanfene/javathread.md) +- [面渣逆袭(Java 虚拟机篇八股文面试题)必看👍](docs/sidebar/sanfene/jvm.md) +- [面渣逆袭(Spring八股文面试题)必看👍](docs/sidebar/sanfene/spring.md) +- [面渣逆袭(Redis八股文面试题)必看👍](docs/sidebar/sanfene/redis.md) +- [面渣逆袭(MyBatis八股文面试题)必看👍](docs/sidebar/sanfene/mybatis.md) +- [面渣逆袭(MySQL八股文面试题)必看👍](docs/sidebar/sanfene/mysql.md) +- [面渣逆袭(操作系统八股文面试题)必看👍](docs/sidebar/sanfene/os.md) +- [面渣逆袭(计算机网络八股文面试题)必看👍](docs/sidebar/sanfene/network.md) +- [面渣逆袭(RocketMQ八股文面试题)必看👍](docs/sidebar/sanfene/rocketmq.md) -- [面渣逆袭(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基础非常重要**!包括基础语法、面向对象、集合框架、异常处理、Java IO、网络编程、NIO、并发编程和 JVM。 +> **Java核心非常重要**!我将其分成了Java 基础篇(包括基础语法、面向对象、集合框架、异常处理、Java IO 等)、Java 并发篇和 Java 虚拟机篇。 -## Java概述及环境配置 +## Java概述 + +- [Java简介,什么是 Java?](docs/overview/what-is-java.md) +- [Java发展简史](docs/overview/java-history.md) +- [Java的特点,Java能做什么](docs/overview/java-can-do-what.md) +- [安装Java开发工具包JDK](docs/overview/jdk-install-config.md) +- [安装集成开发环境Intellij IDEA](docs/overview/IDEA-install-config.md) +- [编写第一个Java程序:Hello World](docs/overview/hello-world.md) -- [《二哥的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中常用的48个关键字 & 2个保留字](docs/basic-extra-meal/48-keywords.md) +- [Java命名规范,告别编码 5 分钟,命名 2 小时](docs/basic-extra-meal/java-naming.md) +- [Java注释:单行、多行和文档注释](docs/basic-grammar/javadoc.md) +- [Java 中的数据类型(8 种基本数据类型和引用数据类型)](docs/basic-grammar/basic-data-type.md) +- [Java 数据类型转换(强制类型转换+自动类型转换)](docs/basic-grammar/type-cast.md) +- [Java流程控制语句](docs/basic-grammar/flow-control.md) +- [Java运算符](docs/basic-grammar/operator.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/array/array.md) +- [如何优雅地打印Java数组?](docs/array/print.md) +- [为什么String是不可变的?](docs/string/immutable.md) +- [深入理解Java字符串常量池](docs/string/constant-pool.md) +- [深入解析String.intern()方法](docs/string/intern.md) +- [Java如何判断两个字符串是否相等?](docs/string/equals.md) +- [最优雅的Java字符串拼接是哪种方式?](docs/string/join.md) +- [如何在Java中优雅地分割String字符串?](docs/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中的类和对象?](docs/oo/object-class.md) +- [Java 包,优雅地解决类名冲突](docs/oo/package.md) +- [Java中的变量:局部变量、成员变量、静态变量、常量](docs/oo/var.md) +- [Java中的方法:实例方法、静态方法、抽象方法](docs/oo/method.md) +- [Java中的构造方法:对象创建时的必经之路](docs/oo/construct.md) +- [聊一聊Java中的访问权限修饰符](docs/oo/access-control.md) +- [Java中的代码初始化块:对成员变量进行更复杂的赋值](docs/oo/code-init.md) +- [Java抽象类,看这一篇就够了,豁然开朗](docs/oo/abstract.md) +- [Java接口,看这一篇就够了,简单易懂](docs/oo/interface.md) +- [Java 抽象类和接口的区别,看这一篇就够了](docs/oo/abstract-vs-interface.md) +- [Java内部类详解:成员内部类、局部内部类、匿名内部类、静态内部类](docs/oo/inner-class.md) +- [Java中this和super的用法总结](docs/oo/this-super.md) +- [Java 方法重写 Override 和方法重载 Overload 的区别,一下子就明白了](docs/basic-extra-meal/override-overload.md) +- [一文彻底搞懂 Java static 关键字:静态变量、静态方法、静态代码块、静态内部类](docs/oo/static.md) +- [一文彻底搞懂 Java final 关键字](docs/oo/final.md) +- [这次彻底搞清楚了 Java 的三大特征之一:封装](docs/oo/encapsulation.md) +- [一万字彻底搞懂 Java 继承(三大特征之一)](docs/oo/extends-bigsai.md) +- [几句话,直观解释清楚 Java 多态(三大特征之一)](docs/oo/polymorphism.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 集合框架(容器)体系结构](docs/collection/gailan.md) +- [衡量Java集合框架执行效率的重要标准:时间复杂度](docs/collection/time-complexity.md) +- [Java ArrayList详解(附源码分析)](docs/collection/arraylist.md) +- [Java LinkedList详解(附源码分析)](docs/collection/linkedlist.md) +- [面试官问我Java中ArrayList和LinkedList的区别,我和他扯了半小时](docs/collection/list-war-2.md) +- [海康威视一面:Java中Iterator和Iterable有什么区别?](docs/collection/iterator-iterable.md) +- [为什么阿里巴巴强制不要在foreach里执行删除操作?还不是因为fail-fast](docs/collection/fail-fast.md) +- [Java HashMap详解(附源码分析)](docs/collection/hashmap.md) +- [Java LinkedHashMap详解(附源码分析)](docs/collection/linkedhashmap.md) +- [Java TreeMap详解(附源码分析)](docs/collection/treemap.md) +- [详解 Java 中的堆和队列(Stack and Queue 附源码分析)](docs/collection/arraydeque.md) +- [详解 Java 中的优先级队列(PriorityQueue 附源码分析)](docs/collection/PriorityQueue.md) +- [Java WeakHashMap详解(附源码分析)](docs/collection/WeakHashMap.md) ## Java 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 IO 不再混乱!](docs/io/shangtou.md) +- [详解 File、Path、Paths、Files 四个类,Java操作文件不再难](docs/io/file-path.md) +- [文件的世界,一切皆字节流(Stream)](docs/io/stream.md) +- [Java字符流Reader和Writer的故事](docs/io/reader-writer.md) +- [Java缓冲流(Buffered):读写速度有了质的飞升](docs/io/buffer.md) +- [Java转换流,解决字符与字节之间编码、解码的乱码问题](docs/io/char-byte.md) +- [Java序列化流,字节和对象之间的序列化和反序列化](docs/io/serialize.md) +- [Java Serializable:明明就一个空的接口嘛](docs/io/Serializbale.md) +- [招银面试官:说说Java transient关键字](docs/io/transient.md) +- [Java打印流:PrintStream & PrintWriter](docs/io/print.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异常处理,YYDS](docs/exception/gailan.md) +- [深入理解 Java 中的 try-with-resouces 语法糖](docs/exception/try-with-resouces.md) +- [Java异常处理的20个最佳实践](docs/exception/shijian.md) +- [Java空指针NullPointerException的传说](docs/exception/npe.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 Arrays:专为数组而生的工具类](docs/common-tool/arrays.md) +- [Java Collections:专为集合框架而生的工具类](docs/common-tool/collections.md) +- [Hutool:国产良心工具包,让你的Java变得更甜](docs/common-tool/hutool.md) +- [Guava:Google开源的工具库,太强大了](docs/common-tool/guava.md) +- [这10个工具类,让我的开发效率提升了50%](docs/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 8 Stream流详细用法](docs/java8/stream.md) +- [Java 8 Optional最佳指南](docs/java8/optional.md) +- [深入浅出Java 8 Lambda表达式](docs/java8/Lambda.md) + +## Java重要知识点 + +- [彻底弄懂Java中的Unicode和UTF-8编码](docs/basic-extra-meal/java-unicode.md) +- [Java中new Integer与Integer.valueOf的区别](docs/basic-extra-meal/int-cache.md) +- [深入剖析Java中的拆箱和装箱](docs/basic-extra-meal/box.md) +- [一文彻底讲明白的Java中的浅拷贝与深拷贝](docs/basic-extra-meal/deep-copy.md) +- [深入理解Java中的hashCode方法](docs/basic-extra-meal/hashcode.md) +- [为什么重写equals方法的时候必须要重写hashCode方法?](docs/basic-extra-meal/equals-hashcode.md) +- [Java重写(Overriding)时应当遵守的11条规则](docs/basic-extra-meal/Overriding.md) +- [Java到底是值传递还是引用传递?](docs/basic-extra-meal/pass-by-value.md) +- [详解Java中Comparable和Comparator接口的区别](docs/basic-extra-meal/comparable-omparator.md) +- [jdk9为什么要将String的底层实现由char数组改成了byte数组?](docs/basic-extra-meal/jdk9-char-byte-string.md) +- [为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)?](docs/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md) +- [Java 中,先有Class还是先有Object?](docs/basic-extra-meal/class-object.md) +- [详解Java中instanceof关键字的用法](docs/oo/instanceof.md) +- [instanceof关键字是如何实现的?](docs/basic-extra-meal/instanceof-jvm.md) +- [深入理解Java中的不可变对象](docs/basic-extra-meal/immutable.md) +- [Java中可变参数的使用](docs/basic-extra-meal/varables.md) +- [深入理解Java中的泛型](docs/basic-extra-meal/generic.md) +- [Java不能实现真正泛型的原因是什么?](docs/basic-extra-meal/true-generic.md) +- [深入理解Java中的注解](docs/basic-extra-meal/annotation.md) +- [新来个技术总监,彻底把 Java 枚举(enum)讲清楚了](docs/basic-extra-meal/enum.md) +- [大白话说清楚Java反射:入门、使用、原理](docs/basic-extra-meal/fanshe.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 Socket:飞鸽传书的网络套接字](docs/socket/socket.md) +- [牛逼,用Java Socket手撸了一个HTTP服务器](docs/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 NIO?](docs/nio/why.md) +- [Java NIO 快速入门(buffer缓冲区、Channel管道、Selector选择器)](docs/nio/rumen.md) +- [一文彻底理解Java IO模型(阻塞IO非阻塞IO/IO多路复用)](docs/nio/moxing.md) +- [使用Java NIO完成网络通信](docs/nio/network-connect.md) +- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](docs/nio/BIONIOAIO.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线程的3种方式](docs/thread/wangzhe-thread.md) +- [Java线程的6种状态及切换(透彻讲解)](docs/thread/thread-state-and-method.md) +- [线程组是什么?线程优先级如何设置?](docs/thread/thread-group-and-thread-priority.md) +- [进程与线程的区别是什么?](docs/thread/why-need-thread.md) +- [并发编程带来了哪些问题?](docs/thread/thread-bring-some-problem.md) +- [全面理解Java的内存模型(JMM)](docs/thread/jmm.md) +- [Java并发编程volatile关键字解析](docs/thread/volatile.md) +- [Java中的synchronized锁的到底是什么?](docs/thread/synchronized.md) +- [Java实现CAS的原理](docs/thread/cas.md) +- [Java并发AQS详解](docs/thread/aqs.md) +- [大致了解下Java的锁接口和锁](docs/thread/lock.md) +- [公司空降一个美团大佬,彻底把Java中的锁”讲清楚了](docs/thread/suo.md) +- [Java 15 终于把难搞的偏向锁移除了](docs/thread/pianxiangsuo.md) +- [深入理解Java并发重入锁ReentrantLock](docs/thread/reentrantLock.md) +- [深入理解Java并发读写锁ReentrantReadWriteLock](docs/thread/ReentrantReadWriteLock.md) +- [深入理解Java并发线程协作类Condition](docs/thread/condition.md) +- [深入理解Java并发线程线程阻塞唤醒类LockSupport](docs/thread/LockSupport.md) +- [简单聊聊Java的并发集合容器](docs/thread/map.md) +- [吊打Java并发面试官之ConcurrentHashMap](docs/thread/ConcurrentHashMap.md) +- [吊打Java并发面试官之ConcurrentLinkedQueue](docs/thread/ConcurrentLinkedQueue.md) +- [吊打Java并发面试官之CopyOnWriteArrayList](docs/thread/CopyOnWriteArrayList.md) +- [吊打Java并发面试官之ThreadLocal](docs/thread/ThreadLocal.md) +- [吊打Java并发面试官之BlockingQueue](docs/thread/BlockingQueue.md) +- [面试必备:Java线程池](docs/thread/pool.md) +- [为什么阿里巴巴要禁用Executors创建线程池?](docs/thread/ali-executors.md) +- [深入剖析Java计划任务ScheduledThreadPoolExecutor](docs/thread/ScheduledThreadPoolExecutor.md) +- [Java atomic包中的原子操作类总结](docs/thread/atomic.md) +- [Java并发编程通信工具类CountDownLatch等一网打尽](docs/thread/CountDownLatch.md) +- [深入理解Java并发编程之Fork/Join框架](docs/thread/fork-join.md) +- [从根上理解生产者-消费者模式](docs/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进阶这部分内容就是一个分水岭**! +- [JVM到底是什么?](docs/jvm/what-is-jvm.md) +- [JVM到底是如何运行Java代码的?](docs/jvm/how-run-java-code.md) +- [我竟然不再抗拒Java的类加载机制了](docs/jvm/class-load.md) +- [详解Java的类文件(class文件)结构](docs/jvm/class-file-jiegou.md) +- [从javap的角度轻松看懂字节码](docs/jvm/bytecode.md) +- [JVM字节码指令详解](docs/jvm/zijiema-zhiling.md) +- [虚拟机是如何执行字节码指令的?](docs/jvm/how-jvm-run-zijiema-zhiling.md) +- [HSDB(Hotspot Debugger)从入门到实战](docs/jvm/hsdb.md) +- [史上最通俗易懂的ASM教程](docs/jvm/asm.md) +- [自己编译JDK](docs/jvm/compile-jdk.md) +- [深入理解JVM的内存结构](docs/jvm/neicun-jiegou.md) +- [Java 创建的对象到底放在哪?](docs/jvm/whereis-the-object.md) +- [咱们从头到尾说一次Java垃圾回收](docs/jvm/gc.md) +- [图解Java的垃圾回收机制](docs/jvm/tujie-gc.md) +- [Java中9种常见的CMS GC问题分析与解决](docs/jvm/meituan-9-gc.md) +- [Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析)](docs/jvm/problem-tools.md) +- [Java即时编译(JIT)器原理解析及实践](docs/jvm/jit.md) +- [一次内存溢出排查优化实战](docs/jvm/oom.md) +- [一次生产CPU 100% 排查优化实践](docs/jvm/cpu-percent-100.md) +- [JVM 核心知识点总结](docs/jvm/zongjie.md) + + +# Java企业级开发 + +> - **到底能不能成为一名合格的 Java 程序员,从理论走向实战?Java 企业级开发这部分内容就是一个分水岭**! > - 纸上得来终觉浅,须知此事要躬行。 -## 开发/构建工具 - -> 工欲善其事必先利其器,这句话大家都耳熟能详了,熟练使用开发/构建工具可以让我们极大提升开发效率,解放生产力。 - -- [5分钟带你深入浅出搞懂Nginx](docs/src/nginx/nginx.md) +## 开发工具 -### IDEA +- [终于把项目构建神器Maven捋清楚了~](docs/maven/maven.md) +- [我在工作中是如何使用Git的](docs/git/git-qiyuan.md) +- [5分钟带你深入浅出搞懂Nginx](docs/nginx/nginx.md) -> 集成开发环境,Java 党主要就是 Intellij IDEA 了,号称史上最强大的 Java 开发工具,没有之一。 +## IDE/编辑器 -- [分享 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) +- [4个高级的IntelliJ IDEA调试技巧](docs/ide/4-debug-skill.md) +- [在 IDEA 里下个五子棋不过分吧?](docs/ide/xechat.md) ## Spring -- [Spring AOP扫盲](docs/src/springboot/aop-log.md) -- [Spring IoC扫盲](docs/src/springboot/ioc.md) +- [Spring AOP扫盲](docs/springboot/aop-log.md) +- [Spring IoC扫盲](docs/springboot/ioc.md) ## 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) +- [一分钟快速搭建Spring Boot项目](docs/springboot/initializr.md) +- [Spring Boot 整合 MySQL 和 Druid](docs/springboot/mysql-druid.md) +- [Spring Boot 整合 JPA](docs/springboot/jpa.md) +- [Spring Boot 整合 Thymeleaf 模板引擎](docs/springboot/thymeleaf.md) +- [Spring Boot 如何开启事务支持?](docs/springboot/transaction.md) +- [Spring Boot 中使用过滤器、拦截器、监听器](docs/springboot/Filter-Interceptor-Listener.md) +- [Spring Boot 整合 Redis 实现缓存](docs/redis/redis-springboot.md) +- [Spring Boot 整合 Logback 定制日志框架](docs/springboot/logback.md) +- [Spring Boot 整合 Swagger-UI 实现在线API文档](docs/springboot/swagger.md) +- [Spring Boot 整合 Knife4j,美化强化丑陋的Swagger](docs/gongju/knife4j.md) +- [Spring Boot 整合 Spring Task 实现定时任务](docs/springboot/springtask.md) +- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](docs/kaiyuan/auto-generator.md) +- [Spring Boot 整合Quartz实现编程喵定时发布文章](docs/springboot/quartz.md) +- [Spring Boot 整合 MyBatis](docs/springboot/mybatis.md) +- [一键部署 Spring Boot 到远程 Docker 容器](docs/springboot/docker.md) +- [如何在本地(macOS环境)跑起来编程喵(Spring Boot+Vue)项目源码?](docs/springboot/macos-codingmore-run.md) +- [如何在本地(Windows环境)跑起来编程喵(Spring Boot+Vue)项目源码?](docs/springboot/windows-codingmore-run.md) +- [编程喵🐱实战项目如何在云服务器上跑起来?](docs/springboot/linux-codingmore-run.md) +- [SpringBoot中处理校验逻辑的两种方式:Hibernate Validator+全局异常处理](docs/springboot/validator.md) ## Netty -- [超详细Netty入门,看这篇就够了!](docs/src/netty/rumen.md) +- [超详细Netty入门,看这篇就够了!](docs/netty/rumen.md) ## 辅助工具 -- [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) +- [Chocolatey:一款GitHub星标8.2k+的Windows命令行软件管理器,好用到爆!](docs/gongju/choco.md) +- [Homebrew,GitHub 星标 32.5k+的 macOS 命令行软件管理神器,功能真心强大!](docs/gongju/brew.md) +- [Tabby:一款逼格更高的开源终端工具,GitHub 星标 21.4k](docs/gongju/tabby.md) +- [Warp:号称下一代终端神器,GitHub星标2.8k+,用完爱不释手](docs/gongju/warp.md) +- [WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了!](docs/gongju/windterm.md) +- [chiner:干掉 PowerDesigner,国人开源的数据库设计工具,界面漂亮,功能强大](docs/gongju/chiner.md) +- [DBeaver:干掉付费的 Navicat,操作所有数据库就靠它了!](docs/gongju/DBeaver.md) ## 开源轮子 -- [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) +- [Forest:一款极简的声明式HTTP调用API框架](docs/gongju/forest.md) +- [Junit:一个开源的Java单元测试框架](docs/gongju/junit.md) +- [fastjson:阿里巴巴开源的JSON解析库](docs/gongju/fastjson.md) +- [Gson:Google开源的JSON解析库](docs/gongju/gson.md) +- [Jackson:GitHub上star数最多的JSON解析库](docs/gongju/jackson.md) +- [Log4j:Java日志框架的鼻祖](docs/gongju/log4j.md) +- [Log4j 2:Apache维护的一款高性能日志记录工具](docs/gongju/log4j2.md) +- [Logback:Spring Boot内置的日志处理框架](docs/gongju/logback.md) +- [SLF4J:阿里巴巴强制使用的日志门面担当](docs/gongju/slf4j.md) ## 分布式 -- [全文搜索引擎Elasticsearch入门教程](docs/src/elasticsearch/rumen.md) -- [可能是把ZooKeeper概念讲的最清楚的一篇文章](docs/src/zookeeper/jibenjieshao.md) -- [微服务网关:从对比到选型,由理论到实践](docs/src/microservice/api-wangguan.md) +- [全文搜索引擎Elasticsearch入门教程](docs/elasticsearch/rumen.md) +- [可能是把ZooKeeper概念讲的最清楚的一篇文章](docs/zookeeper/jibenjieshao.md) +- [微服务网关:从对比到选型,由理论到实践](docs/microservice/api-wangguan.md) ## 消息队列 -- [RabbitMQ入门教程(概念、应用场景、安装、使用)](docs/src/mq/rabbitmq-rumen.md) -- [怎么确保消息100%不丢失?](docs/src/mq/100-budiushi.md) -- [Kafka核心知识点大梳理](docs/src/mq/kafka.md) +- [RabbitMQ入门教程(概念、应用场景、安装、使用)](docs/mq/rabbitmq-rumen.md) +- [怎么确保消息100%不丢失?](docs/mq/100-budiushi.md) +- [Kafka核心知识点大梳理](docs/mq/kafka.md) # 数据库 @@ -423,29 +379,18 @@ ## 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) +- [如何保障MySQL和Redis的数据一致性?](docs/mysql/redis-shuju-yizhixing.md) ## Redis -- [Redis入门(适合新手)](docs/src/redis/rumen.md) -- [聊聊缓存雪崩、穿透、击穿](docs/src/redis/xuebeng-chuantou-jichuan.md) +- [Redis入门(适合新手)](docs/redis/rumen.md) +- [聊聊缓存雪崩、穿透、击穿](docs/redis/xuebeng-chuantou-jichuan.md) ## MongoDB -- [MongoDB最基础入门教程](docs/src/mongodb/rumen.md) +- [MongoDB最基础入门教程](docs/mongodb/rumen.md) # 计算机基础 @@ -453,8 +398,8 @@ > - **计算机基础包括操作系统、计算机网络、计算机组成原理、数据结构与算法等**。对于任何一名想要走得更远的 Java 后端开发来说,都是必须要花时间和精力去夯实的。 > - 万丈高露平地起,勿在浮沙筑高台。 -- [操作系统核心知识点大梳理](docs/src/cs/os.md) -- [计算机网络核心知识点大梳理](docs/src/cs/wangluo.md) +- [操作系统核心知识点大梳理](docs/cs/os.md) +- [计算机网络核心知识点大梳理](docs/cs/wangluo.md) # 求职面试 @@ -464,54 +409,52 @@ ## 面试题&八股文 -- [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) +- [34 道 Java 精选面试题👍](docs/interview/java-34.md) +- [13 道 Java HashMap 精选面试题👍](docs/interview/java-hashmap-13.md) +- [12 道 Redis 精选面试题👍](docs/interview/redis-12.md) +- [40 道 Nginx 精选面试题👍](docs/interview/nginx-40.md) +- [60 道 MySQL 精选面试题👍](docs/interview/mysql-60.md) +- [Java 基础背诵版八股文必看🍉](docs/interview/java-basic-baguwen.md) +- [Java 并发编程背诵版八股文必看🍉](docs/interview/java-thread-baguwen.md) +- [Java 虚拟机背诵版八股文必看🍉](docs/interview/java-jvm-baguwen.md) +- [携程面试官👤:大文件上传时如何做到秒传?](docs/interview/mianshiguan-bigfile-miaochuan.md) +- [阿里面试官👤:为什么要分库分表?](docs/interview/mianshiguan-fenkufenbiao.md) +- [淘宝面试官👤:优惠券系统该如何设计?](docs/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) +- [硕士读者春招斩获深圳腾讯PCG和杭州阿里云 offer✌️](docs/mianjing/shanganaliyun.md) +- [本科读者小公司一年工作经验社招拿下阿里美团头条京东滴滴等 offer✌️](docs/mianjing/shezynmjfxhelmtttjddd.md) +- [非科班读者,用一年时间社招拿下阿里 Offer✌️](docs/mianjing/xuelybdzheloffer.md) +- [二本读者社招两年半10家公司28轮面试面经✌️](docs/mianjing/huanxgzl.md) +- [双非一本秋招收获腾讯ieg、百度、字节等6家大厂offer✌️](docs/mianjing/quzjlsspdx.md) +- [双非学弟收割阿里、字节、B站校招 offer,附大学四年硬核经验总结✌️](docs/mianjing/zheisnylzldhzd.md) +- [深漂 6 年了,回西安的一波面经总结✌️](docs/mianjing/chengxyspnhxagzl.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/nice-article/weixin/miansmtgl.md) +- [有无实习/暑期实习 offer 如何准备秋招?💪](docs/nice-article/weixin/zijxjjdyfqzgl.md) +- [简历如何优化,简历如何投递,面试如何准备?💪](docs/nice-article/weixin/luoczbmsddyb.md) +- [校招时间节点、简历编写、笔试、HR面、实习等注意事项💪](docs/nice-article/weixin/youdxzhhmjzlycfx.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) +- [北京都有哪些值得加入的IT互联网公司?](docs/cityselect/beijing.md) +- [广州都有哪些值得加入的IT互联网公司?](docs/cityselect/guangzhou.md) +- [深圳都有哪些值得加入的IT互联网公司?](docs/cityselect/shenzhen.md) +- [西安都有哪些值得加入的IT互联网公司?](docs/cityselect/xian.md) +- [青岛都有哪些值得加入的IT互联网公司?](docs/cityselect/qingdao.md) +- [郑州都有哪些值得加入的IT互联网公司?](docs/cityselect/zhengzhou.md) +- [苏州都有哪些值得加入的IT互联网公司?](docs/cityselect/suzhou.md) +- [南京都有哪些值得加入的IT互联网公司?](docs/cityselect/nanjing.md) +- [杭州都有哪些值得加入的IT互联网公司?](docs/cityselect/hangzhou.md) +- [成都都有哪些值得加入的IT互联网公司?](docs/cityselect/chengdu.md) +- [济南都有哪些值得加入的IT互联网公司?](docs/cityselect/jinan.md) # 学习资源 @@ -521,86 +464,71 @@ ## PDF下载 -- [👏下载→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) +- [👏下载→超1000本计算机经典书籍分享](docs/pdf/java.md) +- [👏下载→2022年全网最全关于程序员学习和找工作的PDF资源](docs/pdf/programmer-111.md) +- [👏下载→深入浅出Java多线程PDF](docs/pdf/java-concurrent.md) +- [👏下载→GitHub星标115k+的Java教程](docs/pdf/github-java-jiaocheng-115-star.md) +- [👏下载→重学Java设计模式PDF](docs/pdf/shejimoshi.md) +- [👏下载→Java版LeetCode刷题笔记](docs/pdf/java-leetcode.md) +- [👏下载→阿里巴巴Java开发手册](docs/pdf/ali-java-shouce.md) +- [👏下载→阮一峰C语言入门教程](docs/pdf/yuanyifeng-c-language.md) +- [👏下载→BAT大佬的刷题笔记](docs/pdf/bat-shuati.md) +- [👏下载→给操作系统捋条线PDF](docs/pdf/os.md) +- [👏下载→豆瓣9.1分的Pro Git中文版](docs/pdf/progit.md) +- [👏下载→简历模板](docs/pdf/jianli.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) +- [计算机专业该如何自学编程,看哪些书籍哪些视频哪些教程?](docs/xuexijianyi/LearnCS-ByYourself.md) +- [如何阅读《深入理解计算机系统》这本书?](docs/xuexijianyi/read-csapp.md) +- [电子信息工程最好的出路的是什么?](docs/xuexijianyi/electron-information-engineering.md) +- [如何填报计算机大类高考填志愿,计科、人工智能、软工、大数据、物联网、网络工程该怎么选?](docs/xuexijianyi/gaokao-zhiyuan-cs.md) +- [测试开发工程师必读经典书籍有哪些?](docs/xuexijianyi/test-programmer-read-books.md) +- [校招 Java 后端开发应该掌握到什么程度?](docs/xuexijianyi/xiaozhao-java-should-master.md) +- [大裁员下,程序员如何做“副业”?](docs/xuexijianyi/chengxuyuan-fuye.md) +- [如何在繁重的工作中持续成长?](docs/xuexijianyi/ruhzfzdgzzcxcz.md) +- [如何获得高并发的经验?](docs/xuexijianyi/gaobingfa-jingyan-hsmcomputer.md) +- [怎么跟 HR 谈薪资?](docs/xuexijianyi/hr-xinzi.md) +- [程序员 35 岁危机,如何破局?](docs/xuexijianyi/35-weiji.md) +- [不到 20 人的 IT 公司该去吗?](docs/xuexijianyi/20ren-it-quma.md) +- [本科生如何才能进入腾讯、阿里等一流的互联网公司?](docs/xuexijianyi/benkesheng-ali-tengxun.md) +- [计算机考研 408 统考该如何准备?](docs/xuexijianyi/408.md) # 知识库搭建 -> 从购买阿里云服务器+域名购买+域名备案+HTTP 升级到 HTTPS,全方面记录《二哥的Java进阶之路》知识库的诞生和改进过程,涉及到 docsify、Git、Linux 命令、GitHub 仓库等实用知识点。 - -- [购买云服务器](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) +> 从购买阿里云服务器+域名购买+域名备案+HTTP 升级到 HTTPS,全方面记录《Java 程序员进阶之路》知识库的诞生和改进过程,涉及到 docsify、Git、Linux 命令、GitHub 仓库等实用知识点。 -本知识库使用 VuePress 搭建,并基于[VuePress Theme Hope](https://theme-hope.vuejs.press/zh/)主题,你可以把[仓库](https://github.com/itwanger/toBeBetterJavaer)拉到本地后直接通过 `pnpm docs:clean-dev` 跑起来。 ->前提是你已经安装好 node.js 和 pnpm 环境。 +- [购买云服务器](docs/szjy/buy-cloud-server.md) +- [安装宝塔面板](docs/szjy/install-baota-mianban.md) +- [购买域名&域名解析](docs/szjy/buy-domain.md) +- [备案域名](docs/szjy/record-domain.md) +- [给域名配置HTTPS证书](docs/szjy/https-domain.md) +- [使用docsify+Git+GitHub+码云+阿里云服务器搭建知识库网站](docs/szjy/tobebetterjavaer-wangzhan-shangxian.md) -![pnpm 部署进阶之路](https://cdn.paicoding.com/stutymore/README-20241106103513.png) - -点击链接就可以在本地看到运行后的效果了。 - -![二哥的 Java 进阶之路首页](https://cdn.paicoding.com/stutymore/README-20230829162301.png) - -如果想部署服务器,可以执行 `pnpm docs:build` 打包生成 dist 目录,里面就是静态资源文件了。 - -执行 `zip -r dist.zip dist` 压缩为 dist.zip 包,然后上传到服务器的 Nginx 对应的静态资源目录下。再执行 `unzip dist.zip` 解压即可。 # 联系作者 >- 作者是一名普通普通普通普通三连的 Java 后端开发者,热爱学习,热爱分享 >- 参加工作以后越来越理解交流和分享的重要性,在不停地汲取营养的同时,也希望帮助到更多的小伙伴们 ->- 二哥的Java进阶之路,不仅是作者自学 Java 以来所有的原创文章和学习资料的大聚合,更是作者向这个世界传播知识的一个窗口。 +>- Java 程序员进阶之路,不仅是作者自学 Java 以来所有的原创文章和学习资料的大聚合,更是作者向这个世界传播知识的一个窗口。 ## 心路历程 -- [走近作者:个人介绍 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) +- [走近作者:个人介绍 Q&A](docs/about-the-author/readme.md) +- [我的第一个,10 万(B站视频播放)](docs/about-the-author/bzhan-10wan.md) +- [我的第一个,一千万!知乎阅读](docs/about-the-author/zhihu-1000wan.md) +- [我的第二个,一千万!CSDN阅读](docs/about-the-author/csdn-1000wan.md) ## 联系方式 ### 原创公众号 -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ### star趋势图 @@ -610,162 +538,25 @@ GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https ### 友情链接 -- [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! ### 捐赠鼓励 -开源不易,如果《二哥的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|*物|5元 2022-09-23|H*n|1元 2022-09-23|*a|0.01元 2022-09-08|*👀|20元 @@ -778,34 +569,28 @@ GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https 2022-06-10|*❤|1元 2022-06-09|'*'|1元 2022-06-07|*勇|1元 -2022-06-03|*鸭|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-04-12|*生|10元 +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|刚*好|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 元 + + ### 参与贡献 diff --git a/_sidebar.md b/_sidebar.md new file mode 100644 index 0000000000..360c465abf --- /dev/null +++ b/_sidebar.md @@ -0,0 +1,49 @@ +* [为什么会有这个开源知识库](README.md?id=为什么会有这个开源知识库) +* [知识库地图](README.md?id=知识库地图) +* [学习路线](README.md?id=学习路线) +* [Java核心](README.md?id=java核心) + * [Java面渣逆袭](README.md?id=Java面渣逆袭) + * [Java概述](README.md?id=java概述) + * [Java基础语法](README.md?id=java基础语法) + * [Java面向对象编程](README.md?id=Java面向对象编程) + * [字符串&数组](README.md?id=字符串&数组) + * [集合框架(容器)](README.md?id=集合框架(容器)) + * [Java输入输出](README.md?id=Java输入输出) + * [异常处理](README.md?id=异常处理) + * [常用工具类](README.md?id=常用工具类) + * [Java新特性](README.md?id=Java新特性) + * [Java重要知识点](README.md?id=java重要知识点) + * [Java并发编程](README.md?id=Java并发编程) + * [Java虚拟机](README.md?id=Java虚拟机) +* [Java企业级开发](README.md?id=Java企业级开发) + * [开发工具](README.md?id=开发工具) + * [IDE/编辑器](README.md?id=IDE/编辑器) + * [Spring](README.md?id=Spring) + * [SpringBoot](README.md?id=SpringBoot) + * [辅助工具/轮子](README.md?id=辅助工具) + * [安全篇](README.md?id=安全篇) + * [分布式](README.md?id=分布式) + * [高性能](README.md?id=高性能) + * [高可用](README.md?id=高可用) +* [数据库](README.md?id=数据库) + * [MySQL](README.md?id=MySQL) + * [Redis](README.md?id=Redis) + * [MongoDB](README.md?id=MongoDB) +* [计算机基础](README.md?id=计算机基础) +* [求职面试](README.md?id=求职面试) + * [面试题集合](README.md?id=面试题集合) + * [背诵版八股文](README.md?id=背诵版八股文) + * [优质面经](README.md?id=优质面经) + * [面试准备](README.md?id=面试准备) + * [城市选择](README.md?id=城市选择) +* [学习资源](README.md?id=学习资源) + * [PDF下载](README.md?id=PDF下载) + * [图文教程](README.md?id=图文教程) + * [视频教程](README.md?id=视频教程) + * [优质书单](README.md?id=优质书单) + * [学习建议](README.md?id=学习建议) +* [知识库建设](README.md?id=知识库建设) +* [联系作者](README.md?id=联系作者) + * [心路历程](README.md?id=心路历程) + * [联系方式](README.md?id=联系方式) + diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts new file mode 100644 index 0000000000..f3cf46d46c --- /dev/null +++ b/docs/.vuepress/config.ts @@ -0,0 +1,148 @@ +import { docsearchPlugin } from "@vuepress/plugin-docsearch"; +import { commentPlugin } from "vuepress-plugin-comment2"; +import { pwaPlugin } from "vuepress-plugin-pwa2"; +import { defineUserConfig } from "vuepress"; +import theme from "./theme"; + +export default defineUserConfig({ + base: "/", + + dest: "./dist", + + // 如果你正在使用本插件,我们推荐在你的 VuePress 配置文件中设置 + shouldPrefetch: false, + // 自动 description + autoDescription: true, + // 精选图标 https://vuepress-theme-hope.github.io/v2/zh/guide/interface/icon.html#%E4%BD%BF%E7%94%A8 + iconAssets: "iconfont", + + head: [ + [ + "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); + })(); + ` + ], + [ + "link", + { + rel: "stylesheet", + href: "//at.alicdn.com/t/font_3180624_7cy10l7jqqh.css", + }, + ], + ], + + locales: { + "/": { + lang: "zh-CN", + title: "Java 程序员进阶之路", + description: "一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、Java企业级开发、Java面试等核心知识点。学Java,就认准Java程序员进阶之路", + }, + }, + + plugins: [ + // Progressive Web app,即渐进式网络应用程序, + // 允许网站通过支持该特性的浏览器将网站作为 App 安装在对应平台上。 + pwaPlugin({ + // favicon.ico一般用于作为缩略的网站标志,它显示位于浏览器的地址栏或者在标签上,用于显示网站的logo, + favicon: "/favicon.ico", + // 如果你的站点体积不大,且配图大多为关键性说明,希望可以在离线模式下显示,建议将此项设置为 true + cachePic: 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", + }, + ], + }, + }), + commentPlugin({ + provider: "Giscus", + repo :"itwanger/tobebetterjavaer-giscus", + repoId:"R_kgDOHBJssg", + category:"Announcements", + categoryId:"DIC_kwDOHBJsss4COJOx", + }), + docsearchPlugin({ + 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: "你可以尝试查询", + }, + }, + }, + }, + }, + }), + ], + + theme, +}); diff --git a/docs/.vuepress/navbar.ts b/docs/.vuepress/navbar.ts new file mode 100644 index 0000000000..37e39839a7 --- /dev/null +++ b/docs/.vuepress/navbar.ts @@ -0,0 +1,50 @@ +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: "youzhi", + children: [ + { + text: "PDF下载", + icon: "java", + link: "/pdf/readme.md" + }, + { + text: "面渣逆袭", + icon: "zhunbei", + link: "/sidebar/sanfene/nixi.md" + }, + { + text: "破解合集", + icon: "zhongyaotishi", + link: "/nice-article/itmind/" + }, + ], + }, + { + text: "B站视频", + icon: "bzhan", + link: "https://space.bilibili.com/513340480" + }, +]); diff --git a/docs/src/.vuepress/public/assets/icon/apple-icon-152.png b/docs/.vuepress/public/assets/icon/apple-icon-152.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/apple-icon-152.png rename to docs/.vuepress/public/assets/icon/apple-icon-152.png diff --git a/docs/src/.vuepress/public/assets/icon/chrome-192.png b/docs/.vuepress/public/assets/icon/chrome-192.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/chrome-192.png rename to docs/.vuepress/public/assets/icon/chrome-192.png diff --git a/docs/src/.vuepress/public/assets/icon/chrome-512.png b/docs/.vuepress/public/assets/icon/chrome-512.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/chrome-512.png rename to docs/.vuepress/public/assets/icon/chrome-512.png diff --git a/docs/src/.vuepress/public/assets/icon/chrome-mask-192.png b/docs/.vuepress/public/assets/icon/chrome-mask-192.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/chrome-mask-192.png rename to docs/.vuepress/public/assets/icon/chrome-mask-192.png diff --git a/docs/src/.vuepress/public/assets/icon/chrome-mask-512.png b/docs/.vuepress/public/assets/icon/chrome-mask-512.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/chrome-mask-512.png rename to docs/.vuepress/public/assets/icon/chrome-mask-512.png diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-282.png b/docs/.vuepress/public/assets/icon/itwanger-282.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/itwanger-282.png rename to docs/.vuepress/public/assets/icon/itwanger-282.png diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-maskable.png b/docs/.vuepress/public/assets/icon/itwanger-maskable.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/itwanger-maskable.png rename to docs/.vuepress/public/assets/icon/itwanger-maskable.png diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-monochrome.png b/docs/.vuepress/public/assets/icon/itwanger-monochrome.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/itwanger-monochrome.png rename to docs/.vuepress/public/assets/icon/itwanger-monochrome.png diff --git a/docs/src/.vuepress/public/assets/icon/ms-icon-144.png b/docs/.vuepress/public/assets/icon/ms-icon-144.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/ms-icon-144.png rename to docs/.vuepress/public/assets/icon/ms-icon-144.png diff --git a/docs/src/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico similarity index 100% rename from docs/src/.vuepress/public/favicon.ico rename to docs/.vuepress/public/favicon.ico diff --git a/docs/src/.vuepress/public/itwanger.png b/docs/.vuepress/public/itwanger.png similarity index 100% rename from docs/src/.vuepress/public/itwanger.png rename to docs/.vuepress/public/itwanger.png diff --git a/docs/src/.vuepress/public/itwanger.svg b/docs/.vuepress/public/itwanger.svg similarity index 100% rename from docs/src/.vuepress/public/itwanger.svg rename to docs/.vuepress/public/itwanger.svg diff --git a/docs/src/.vuepress/public/logo.png b/docs/.vuepress/public/logo.png similarity index 100% rename from docs/src/.vuepress/public/logo.png rename to docs/.vuepress/public/logo.png diff --git a/docs/src/.vuepress/public/logo.svg b/docs/.vuepress/public/logo.svg similarity index 100% rename from docs/src/.vuepress/public/logo.svg rename to docs/.vuepress/public/logo.svg diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts new file mode 100644 index 0000000000..0ff1448671 --- /dev/null +++ b/docs/.vuepress/sidebar.ts @@ -0,0 +1,697 @@ +import { sidebar } from "vuepress-theme-hope"; +export const sidebarConfig = sidebar({ + "/zhishixingqiu/": [ + "readme.md", + "map", + "may", + "june", + "july", + "august" + ], + "/nice-article/itmind/": [ + "readme", + "ideapxideajhideayjjhmideazxjhzcmpjjcyjjhqcyx", + "yigkymxczideatsyqdffblwxjcywdxbxt", + "ideapjazjczxjhmzcmyjjhcxgxz", + "ideajhmideajhmideapxideajhmideazcmideayjjhm", + "navicatmacyjpx", + "navicatzxbwindowspjbjc", + "typorayjpx", + "typoramaczwpjbhyjjdkptmarkdownbjqmksimacsocom", + "xshellazpjbjcxshellpxffxbxt", + "pycharmjhpxmazjcnyrgxxbxt", + "yang1", + ], + "/pdf/": [ + "java", + "programmer-111", + "java-concurrent", + "github-java-jiaocheng-115-star", + "shejimoshi", + "java-leetcode", + "ali-java-shouce", + "yuanyifeng-c-language", + "bat-shuati", + "os", + "progit", + "jianli", + ], + "/xuexiluxian/": [ + { + text: "Java学习路线", + prefix: "java/", + collapsable: true, + children: [ + "yitiaolong", + "thread", + "jvm", + ], + }, + "c", + "ccc", + "python", + "go", + "os", + "qianduan", + "algorithm", + "lanqiaobei", + "bigdata", + "android", + ], + "/sidebar/sanfene/": [ + "nixi", + "javase", + "collection", + "javathread", + "jvm", + "spring", + "redis", + "mybatis", + "mysql", + "os", + "network", + "rocketmq", + ], + // 必须放在最后面 + "/": [ + { + text: "一、前言", + link: "home", + }, + { + text: "二、Java核心", + collapsable: true, + children: [ + { + prefix: "overview/", + text: "2.1 Java概述", + collapsable: true, + children: [ + "what-is-java", + "java-history", + "java-can-do-what", + "jdk-install-config", + "IDEA-install-config", + "hello-world", + ], + }, + { + text: "2.2 Java语法基础", + collapsable: true, + children: [ + "basic-extra-meal/48-keywords", + "basic-extra-meal/java-naming", + "basic-grammar/javadoc", + "basic-grammar/basic-data-type", + "basic-grammar/type-cast", + "basic-grammar/operator", + "basic-grammar/flow-control", + ], + }, + { + text: "2.3 数组&字符串", + collapsable: true, + children: [ + "array/array", + "array/print", + "string/immutable", + "string/constant-pool", + "string/intern", + "string/equals", + "string/join", + "string/split", + ], + }, + { + text: "2.4 面向对象编程", + collapsable: true, + children: [ + "oo/object-class", + "oo/package", + "oo/var", + "oo/method", + "oo/construct", + "oo/access-control", + "oo/code-init", + "oo/abstract", + "oo/interface", + "oo/abstract-vs-interface", + "oo/inner-class", + "oo/this-super", + "basic-extra-meal/override-overload", + "oo/static", + "oo/final", + "oo/encapsulation", + "oo/extends-bigsai", + "oo/polymorphism", + + ], + }, + { + text: "2.5 集合框架(容器)", + collapsable: true, + children: [ + + "collection/gailan", + "collection/arraylist", + "collection/linkedlist", + "collection/list-war-2", + "collection/iterator-iterable", + "collection/fail-fast", + "collection/hashmap", + "collection/linkedhashmap", + "collection/treemap", + "collection/arraydeque", + "collection/PriorityQueue", + "collection/WeakHashMap", + + ], + }, + { + text: "2.6 IO", + collapsable: true, + prefix:"io/", + children: [ + "shangtou", + "file-path", + "stream", + "reader-writer", + "buffer", + "char-byte", + "serialize", + "Serializbale", + "transient", + "print", + ], + }, + { + text: "2.7 异常处理", + collapsable: true, + prefix:"exception/", + children: [ + "gailan", + "try-with-resouces", + "shijian", + "npe", + ], + }, + { + text: "2.8 常用工具类", + collapsable: true, + prefix:"common-tool/", + children: [ + "arrays", + "collections", + "hutool", + "guava", + "utils", + ], + }, + { + text: "2.9 Java新特性", + prefix: "java8/", + collapsable: true, + children: [ + "stream", + "optional", + "Lambda", + ], + }, + { + text: "2.10 Java重要知识点", + prefix:"basic-extra-meal/", + collapsable: true, + children: [ + "java-unicode", + "int-cache", + "box", + "deep-copy", + "hashcode", + "equals-hashcode", + "Overriding", + "pass-by-value", + "comparable-omparator", + "jdk9-char-byte-string", + "jdk-while-for-wuxian-xunhuan", + "class-object", + "instanceof", + "instanceof-jvm", + "immutable", + "varables", + "generic", + "true-generic", + "annotation", + "enum", + "fanshe", + ], + }, + { + text: "2.11 网络编程", + collapsable: true, + prefix: "socket/", + children: [ + "socket", + "http", + ], + }, + { + text: "2.12 NIO", + collapsable: true, + prefix: "nio/", + children: [ + "why", + "rumen", + "moxing", + "network-connect", + "BIONIOAIO", + ], + }, + { + text: "2.13 并发编程", + collapsable: true, + prefix: "thread/", + children: [ + "wangzhe-thread", + "thread-state-and-method", + "thread-group-and-thread-priority", + "why-need-thread", + "thread-bring-some-problem", + "jmm", + "volatile", + "synchronized-1", + "synchronized", + "cas", + "aqs", + "lock", + "suo", + "pianxiangsuo", + "reentrantLock", + "ReentrantReadWriteLock", + "condition", + "LockSupport", + "map", + "ConcurrentHashMap", + "ConcurrentLinkedQueue", + "CopyOnWriteArrayList", + "ThreadLocal", + "BlockingQueue", + "pool", + "ScheduledThreadPoolExecutor", + "atomic", + "CountDownLatch", + "fork-join", + "shengchanzhe-xiaofeizhe", + ], + }, + { + text: "2.14 JVM", + prefix: "jvm/", + collapsable: true, + children: [ + "what-is-jvm", + "how-run-java-code", + "class-load", + "class-file-jiegou", + "bytecode", + "zijiema-zhiling", + "how-jvm-run-zijiema-zhiling", + "hsdb", + "asm", + "compile-jdk", + "neicun-jiegou", + "whereis-the-object", + "gc", + "tujie-gc", + "meituan-9-gc", + "problem-tools", + "jit", + "oom", + "cpu-percent-100", + "zongjie", + + ], + }, + ], + }, + { + text: "三、Java企业级开发", + collapsable: true, + children: [ + { + text: "3.1 开发工具", + collapsable: true, + children: [ + "maven/maven", + "git/git-qiyuan", + "nginx/nginx", + ], + }, + { + text: "3.2 IDE/编辑器", + collapsable: true, + children: [ + "ide/4-debug-skill", + "ide/xechat", + ], + }, + { + text: "3.3 Spring", + collapsable: true, + children: [ + { + text: "Spring AOP扫盲", + link: "springboot/aop-log", + }, + { + text: "Spring IoC扫盲", + link: "springboot/ioc", + }, + ], + }, + { + text: "3.4 SpringBoot", + collapsable: true, + children: [ + { + text: "搭建第一个Spring Boot项目", + link: "springboot/initializr", + }, + { + 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.5 Netty", + collapsable: true, + children: [ + "netty/rumen", + ], + }, + { + text: "3.6 辅助工具", + collapsable: true, + children: [ + "gongju/choco", + "gongju/brew", + "gongju/tabby", + "gongju/warp", + "gongju/windterm", + "gongju/chiner", + "gongju/DBeaver", + ], + }, + { + text: "3.7 开源轮子", + collapsable: 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.8 分布式", + collapsable: true, + children: [ + { + text: "Elasticsearch入门", + link: "elasticsearch/rumen" + }, + { + text: "聊聊ZooKeeper", + link: "zookeeper/jibenjieshao" + }, + { + text: "聊聊微服务网关", + link: "microservice/api-wangguan" + }, + ], + }, + { + text: "3.9 消息队列", + collapsable: true, + children: [ + { + text: "RabbitMQ入门", + link: "mq/rabbitmq-rumen" + }, + { + text: "如何保障消息不丢失", + link: "mq/100-budiushi" + }, + "mq/kafka", + ], + }, + ], + }, + { + text: "四、数据库", + collapsable: true, + children: [ + { + text: "MySQL", + collapsable: true, + children: [ + { + text: "MySQL和Redis数据一致性", + link: "mysql/redis-shuju-yizhixing" + }, + ], + }, + { + text: "Redis", + collapsable: true, + children: [ + { + text: "Redis入门", + link: "redis/rumen" + }, + { + text: "缓存雪崩、穿透、击穿", + link: "redis/xuebeng-chuantou-jichuan" + }, + ], + }, + { + text: "MongoDB", + collapsable: true, + children: [ + "mongodb/rumen", + ], + }, + ], + }, + { + text: "五、计算机基础", + collapsable: true, + prefix: "cs/", + children: [ + { + text: "计算机操作系统", + link: "os", + }, + { + text: "计算机网络", + link: "wangluo", + }, + ], + }, + { + text: "六、求职面试", + collapsable: true, + children: [ + { + text: "面试题&八股文", + collapsable: true, + prefix:"interview/", + children: [ + "java-34", + "java-hashmap-13", + "redis-12", + "mysql-60", + "java-basic-baguwen", + "java-thread-baguwen", + "java-jvm-baguwen", + "mianshiguan-bigfile-miaochuan", + "mianshiguan-fenkufenbiao", + "mianshiguan-youhuiquan", + ], + }, + { + text: "优质面经", + collapsable: true, + prefix:"mianjing/", + children: [ + "shanganaliyun", + "shezynmjfxhelmtttjddd", + "xuelybdzheloffer", + "huanxgzl", + "quzjlsspdx", + "zheisnylzldhzd", + "chengxyspnhxagzl", + ], + }, + { + text: "面试准备", + collapsable: true, + children: [ + "nice-article/weixin/zijxjjdyfqzgl", + "nice-article/weixin/miansmtgl", + "nice-article/weixin/luoczbmsddyb", + "nice-article/weixin/youdxzhhmjzlycfx", + "nice-article/weixin/zheisnylzldhzd", + ], + }, + { + text: "城市选择", + prefix: "cityselect/", + collapsable: true, + children: [ + "beijing", + "chengdu", + "guangzhou", + "hangzhou", + "nanjing", + "qingdao", + "shenzhen", + "suzhou", + "xian", + "zhengzhou", + "jinan", + ], + }, + ], + }, + { + text: "七、学习建议", + collapsable: 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: "八、知识库搭建", + collapsable: true, + prefix: "szjy/", + children: [ + "buy-cloud-server", + "install-baota-mianban", + "buy-domain", + "record-domain", + "https-domain", + ], + }, + { + text: "九、联系作者", + collapsable: true, + prefix: "about-the-author/", + children: [ + "bzhan-10wan", + "zhihu-1000wan", + "csdn-1000wan", + "readme", + + ], + }, + ], +}); + + + diff --git a/docs/.vuepress/styles/config.scss b/docs/.vuepress/styles/config.scss new file mode 100644 index 0000000000..a1b7f5ba10 --- /dev/null +++ b/docs/.vuepress/styles/config.scss @@ -0,0 +1,2 @@ +$code-light-theme: "one-light"; +$code-dark-theme: "one-dark"; \ No newline at end of file diff --git a/.claude/skills/ai-article/sucai.md b/docs/.vuepress/styles/index.scss similarity index 100% rename from .claude/skills/ai-article/sucai.md rename to docs/.vuepress/styles/index.scss diff --git a/docs/.vuepress/styles/palette.scss b/docs/.vuepress/styles/palette.scss new file mode 100644 index 0000000000..d642be5597 --- /dev/null +++ b/docs/.vuepress/styles/palette.scss @@ -0,0 +1,4 @@ +// colors +$theme-color: #5b86ff; +$sidebar-mobile-width: 16rem; +$sidebar-width: 22rem; \ No newline at end of file diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts new file mode 100644 index 0000000000..8dd4b25f2f --- /dev/null +++ b/docs/.vuepress/theme.ts @@ -0,0 +1,106 @@ +import { hopeTheme } from "vuepress-theme-hope"; +import navbar from "./navbar"; +import { sidebarConfig } from "./sidebar"; + +export default hopeTheme({ + hostname: "https://tobebetterjavaer.com", + + encrypt: { + config: { + // 这只会加密 config/page.html + "/nice-article/itmind/xshellazpjbjcxshellpxffxbxt.html": ["1110", "5210"], + "/nice-article/itmind/ideapjazjczxjhmzcmyjjhcxgxz.html": ["1110", "5210"], + }, + }, + + author: { + name: "沉默王二", + url: "/about-the-author/", + }, + + encryptLocales: { + /** + * Encrypt title + */ + title: "初次访问,人机识别活动下筋骨", + placeholder: "微信搜‘沉默王二’回复‘密码’获取口令", + + /** + * Passwrod error hint + */ + errorHint: "哈哈,别调戏人家啦,按规则来嘛", + }, + + iconPrefix: "iconfont icon-", + + logo: "http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/logo-02.png", + + repo: "https://github.com/itwanger/toBeBetterJavaer", + + docsDir: "docs", + + // 以前的默认仓库分支 + docsBranch: "master", + + // 纯净模式 + // pure: true, + + darkmode: "switch", + + // navbar + navbar: navbar, + + // sidebar + sidebar: sidebarConfig, + + footer: '豫ICP备2021038026号-1' + +'' + +'' + +'豫公网安备 41030502000411号' + +'', + + displayFooter: true, + + pageInfo: ["Author", "Original", "Date", "Category", "Tag", "ReadingTime"], + + // page meta + metaLocales: { + editLink: "在 GitHub 上编辑此页", + }, + + blog: { + intro: "/about-the-author/", + sidebarDisplay: "mobile", + // 博主头像 + avatar: "/assets/icon/itwanger-282.png", + // 圆角 + roundAvatar: true, + // 座右铭 + description:"没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。", + medias: { + Zhihu: "https://www.zhihu.com/people/cmower", + Github: "https://github.com/itwanger", + Gitee: "https://gitee.com/itwanger", + }, + + }, + + plugins: { + // 启用博客功能 + blog: true, + // 启用博客自动摘要 + blog: { + autoExcerpt: true, + }, + + activeHeaderLinks: true, + + mdEnhance: { + // 仅将此选项用于体验或测试。 + align: true, + presentation: { + plugins: ["highlight", "math", "search", "notes", "zoom"], + }, + }, + }, +}); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..454861af59 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,93 @@ +--- +home: true +icon: home +title: 主页 +heroImage: http://cdn.tobebetterjavaer.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 +--- + + +## 推荐阅读 + +- [学习路线👉](/xuexiluxian/) : 一份涵盖 Java、C 语言、C++、Python、Go、操作系统、前端、数据结构与算法、蓝桥杯、大数据、Android的全方位编程学习路线!清晰且有效! +- [面渣逆袭📗](sidebar/sanfene/nixi.md) :面试前必刷,硬核理解版八股文,包括 Java 基础(JavaSE)、Java 集合框架、Java 并发编程(Java 多线程)、Java 虚拟机(JVM)、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ等等,助你拿到心仪 offer! +- [Java程序员常读书单📚](/pdf/):超1000本PDF,助力Java 程序员构建最强知识体系,涵盖Java、设计模式、数据库、数据结构与算法、大数据、架构、其他编程语言等等,再也不用辛苦去找下载地址了,这里全都有。 +- [编程喵喵🐱实战项目学习教程](https://github.com/itwanger/codingmore-learning) :codingmore(Spring Boot+Vue 的前后端分离项目,一款值得一试的开源知识库学习网站)的学习教程,需要项目经验的 Java 开发者必备! + +## 公众号 + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.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: 感谢大家对我资金的赞赏,每隔一个月会统计一次。 + +时间|小伙伴|赞赏金额 +---|---|--- +2022-09-30|*君|1元 +2022-09-28|*懂|1元 +2022-09-27|*府|1元 +2022-09-23|*物|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|*生|10元 +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|马*谊|6.6元 +2022-01-01|刚*好|3.6元 +2021-12-20|t*1|5 元 +2021-10-26|*鱼|28 元 +2021-10-11|*人|28 元 +2021-09-01|S*n|6.6 元 +2021-08-02|*秒|1 元 +2021-06-13|*7| 28 元 +2021-04-29|p*e|2 元 + + + +## 参与贡献 + +1. 如果你对本项目有任何建议或发现文中内容有误的,欢迎提交 issues 进行指正。 +2. 对于文中我没有涉及到知识点,欢迎提交 PR。 \ No newline at end of file diff --git a/docs/src/about-the-author/bzhan-10wan.md b/docs/about-the-author/bzhan-10wan.md similarity index 89% rename from docs/src/about-the-author/bzhan-10wan.md rename to docs/about-the-author/bzhan-10wan.md index 503848d6d3..acfb32f166 100644 --- a/docs/src/about-the-author/bzhan-10wan.md +++ b/docs/about-the-author/bzhan-10wan.md @@ -10,13 +10,13 @@ tag: 恭喜这个 B。。。。。。站上的 UP,上一期视频播放量突破了 10 万!这也是二哥人生当中的第一次,凭借单条视频突破 10 万播放,必须得纪念下。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-4f27a848-7dba-4cd3-a705-a6ef02162338.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-340f1fe4-49f1-48c6-a972-c5da7de8ddd0.png) 我已经按捺不住激动的心情,在两万人的朋友圈大肆炫耀了。十万播放,对于百大 UP 来说,可能就是分分钟的事,可对于我这个(未来的) B站百大来说,苦苦等了 149 天!!!!!!! @@ -36,13 +36,13 @@ tag: 三不三连没关系,有关系的是不三连可能会对不起二哥的肝,所以还是三连吧,哈哈哈哈,瞧瞧我们这该死的生物钟,起这么早。。。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-5a1d423e-8827-4a66-9197-4641ef0ecbaf.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-99073995-10b0-42ee-81e5-cc4abc26aa71.png) 啊,不,还是要稍微卷一卷吧,免得被那群还有半年就毕业的家伙们拍死在沙滩上。。。。 @@ -60,7 +60,7 @@ tag: 所以我的建议是,**趁寒假打打王者上上分吧**! -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-63412a28-7315-4ac4-a04b-a049b338c0d8.gif) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-63412a28-7315-4ac4-a04b-a049b338c0d8.gif) 啊,不!**趁寒假刷一波清华在 GitHub 上 20k+ star 的开源课程吧**! @@ -70,7 +70,7 @@ tag: >地址:https://github.com/PKUanonym/REKCARC-TSC-UHT -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-c309fdb8-084c-44b1-bd8e-86a4b39cbb7b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-c309fdb8-084c-44b1-bd8e-86a4b39cbb7b.png) 我来带小伙伴们过一下清华的课程安排哈,主要是针对计算机专业的。 @@ -105,7 +105,7 @@ tag: 学完这些,大家至少能学会下面这幅思维导图中列出来的内容。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-03e4f5b4-c756-401c-aada-695b9cfaf00d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-03e4f5b4-c756-401c-aada-695b9cfaf00d.png) 更多 C 语言的学习内容,可以戳下面这个链接,之前在公众号上发过了,这里就不再复制粘贴了: @@ -190,7 +190,7 @@ class 二哥 { 第一本,《趣学数据结构》 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-bd02be2f-ae71-413f-b0c0-0050fee0e2b5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-bd02be2f-ae71-413f-b0c0-0050fee0e2b5.png) 说到这,多说一嘴。2018 年的时候,人民邮电出版社的张老师邀请我出一本 Java 方面的书,我当时想命名为《趣学 Java》。张老师说,刚好之前和陈小玉老师合作出了一本《趣学算法》的书,要不发一本你看看吧。 @@ -198,11 +198,11 @@ class 二哥 { 第二本,《数据结构(C++语言版)》 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-de28bb8a-ddb6-4b73-b132-ebf5e507fdbe.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-15de0615-3d9c-4bdc-bedb-4165ac6f4802.png) 课程质量木得说,算是国家级精品课了。大家有时间的话,一定要刷一遍。 @@ -217,13 +217,13 @@ class 二哥 { 第一本,《数据结构与算法分析(Java 语言描述)》 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-1672e79d-a576-4a24-bc60-ced47b692a0f.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-9e260fc8-69fa-4dfa-82c6-9d6b1d7027e5.png) 虽然名为算法,但大家都知道,算法是基于数据结构的,数组、队列、栈、堆、二叉树、哈希表等等,这些数据结构都讲到了。 diff --git a/docs/src/about-the-author/csdn-1000wan.md b/docs/about-the-author/csdn-1000wan.md similarity index 77% rename from docs/src/about-the-author/csdn-1000wan.md rename to docs/about-the-author/csdn-1000wan.md index fce9224f7b..5802693f65 100644 --- a/docs/src/about-the-author/csdn-1000wan.md +++ b/docs/about-the-author/csdn-1000wan.md @@ -14,15 +14,15 @@ tag: 我努力的回想着,回想自己在 2021 年做出了哪些耀眼的成绩,正襟危坐,回想良久,也只想到这最后一件:**CSDN 的博文访问量也突破了一千万**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-2627c2a4-46c1-49a9-b86f-607c65ba6398.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-43677bfa-bdd6-4c3b-aa67-3a3277dc3575.png) 那时候的 JavaEye 真的是非常非常非常的纯粹(比博客园更纯粹),没有任何商业广告,还时不时送送书,头部作者有 fastjson 的作者温少,《亿级流量网站架构核心技术》作者开涛,想必做技术的大家应该都知道他们俩。 @@ -30,13 +30,13 @@ tag: 就这样写着写着,我成了 CSDN 的博客专家,出版了一本技术图书,成为了两届博客之星。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-5137a245-035e-4e36-ba04-2b06fae275e1.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-11df1e5d-2505-416c-949a-607b4ebbc61f.png) 经常有读者夸赞二哥好有写作的天赋啊,其实哪里是有天赋,纯粹是因为写得多,所以才写得好。我现在的文笔,讲真,还不如上高中那会,那会才是真的笔下生花,诗都能写得出来,情书就更不用说了。 @@ -50,47 +50,47 @@ tag: 2 月 15 日,我和奶奶的合影。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-14d7737b-7324-4717-bd16-a3da7f2b1223.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-0056fcd2-afa4-475e-ad69-95b84b90b8c7.png) -10 月 11 日,收到《二哥的Java进阶之路》专栏在 GitHub 上开源以来的两笔大额打赏。 +10 月 11 日,收到《Java 程序员进阶之路》专栏在 GitHub 上开源以来的两笔大额打赏。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-7e6d9918-6d94-45ef-9afa-42882f79944d.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-a070af45-bfeb-4b1a-9b29-745279a4a0fc.png) 不知道大家的 2021 过得怎么样? @@ -107,7 +107,7 @@ tag: 一年时间过得可真快,有很多想做好的事情,到最后都差了点意思。就说一件吧,B 站的视频播放量没有达到预期。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-83e7cfbc-496d-45e8-bfd6-cd0d1dfc60d3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-83e7cfbc-496d-45e8-bfd6-cd0d1dfc60d3.png) 8 月份还能坚持一周输出一个,从一开始面对镜头时的恐惧,到慢慢接纳自己。但好景不长,9 月份的时候,视频播放量呈现下降趋势,我就开始胡思乱想了。 @@ -159,21 +159,21 @@ B 站我一定做到一万粉——这个 flag 不能到。 展望 2022 年,有太多的期待了。 -这不,新的惊喜就是《二哥的Java进阶之路》专栏第一次上了 GitHub 的 trending 榜单! +这不,新的惊喜就是《Java 程序员进阶之路》专栏第一次上了 GitHub 的 trending 榜单! -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-8868c50f-d622-4f1f-a92b-13cf95edd786.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-8868c50f-d622-4f1f-a92b-13cf95edd786.png) 正应了那句话,功夫不负有心人。 对于这个开源专栏,我投入了大量的心血,一开始的名字叫《教妹学 Java》,主打 Java 的入门级路线,一直连载了近 100篇原创内容。 -后来有朋友建议我,要想走国际化的话,就必须得换个名字,得和国际接轨,于是我就想破脑袋,征求了很多朋友的建议,改成这个《二哥的Java进阶之路》了,因为我之前出版过一本技术书《Web 全栈开发进阶之路》,叫这个名字刚好也非常适合。 +后来有朋友建议我,要想走国际化的话,就必须得换个名字,得和国际接轨,于是我就想破脑袋,征求了很多朋友的建议,改成这个《Java 程序员进阶之路》了,因为我之前出版过一本技术书《Web 全栈开发进阶之路》,叫这个名字刚好也非常适合。 英文名字叫 toBeBetterJavaer, 前后呼应,一气呵成。 立个 flag 吧,**2022 年,冲 5000 star**! -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-5ebc2c65-f342-45f9-aaf6-7f663b5406b8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-5ebc2c65-f342-45f9-aaf6-7f663b5406b8.png) 这个 flag 绝不能倒! diff --git a/docs/about-the-author/readme.md b/docs/about-the-author/readme.md new file mode 100644 index 0000000000..e0e17e0fd3 --- /dev/null +++ b/docs/about-the-author/readme.md @@ -0,0 +1,103 @@ +--- +title: 个人介绍 Q&A +category: 联系作者 +--- + +大家好,我是二哥呀!这篇文章会通过 QA 的形式简单介绍一下我自己。 + +## 一、我取得了哪些成绩? + +又到了晒成绩的环节,真让人迫不及待啊(瞧我这该死的自信)! + +### 01、公众号 + +目前我的原创公众号“**沉默王二**”有 10.5 万+ 读者关注,专注于分享硬核的 Java 后端技术文章。平均阅读 5500 左右,综合排名能排在全国开发者中的前 50 名左右(数据来源于二十次幂)。 + +可以微信搜索 **沉默王二** 关键字或者扫码直接关注,关注后回复 **00** 还可以拉取我为你精心准备的学习资料。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +学习资料有 BAT 大佬的刷题笔记,有《Java 程序员进阶之路》的 PDF 版电子书等等。 + +![](http://cdn.tobebetterjavaer.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) + +![](http://cdn.tobebetterjavaer.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) + +![](http://cdn.tobebetterjavaer.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) + +![](http://cdn.tobebetterjavaer.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) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-aa477206-41a9-4c55-a649-3d87ba1cb26b.png) + + +### 05、知识星球 + +目前还处在试运营阶段,正在筹备星球用户专属的 5 份小册,质量高的一笔。 + +![](http://cdn.tobebetterjavaer.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 元)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-066ef990-a603-4ace-9a19-728eeb319924.png) + +还有星球内部我也在坚持每天更新优质的内容。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-e108c929-ebc5-4d75-8d40-825f6d027117.png) + +喜欢的小伙伴可以直接扫码加入。 + +![](http://cdn.tobebetterjavaer.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://tobebetterjavaer.com](https://tobebetterjavaer.com) + +## 四、未完待续 diff --git a/docs/src/about-the-author/zhihu-1000wan.md b/docs/about-the-author/zhihu-1000wan.md similarity index 88% rename from docs/src/about-the-author/zhihu-1000wan.md rename to docs/about-the-author/zhihu-1000wan.md index 4fc276dbde..6c3f4b845f 100644 --- a/docs/src/about-the-author/zhihu-1000wan.md +++ b/docs/about-the-author/zhihu-1000wan.md @@ -13,7 +13,7 @@ tag: 前几天,偷偷摸摸过了自己的第 N 个 18 岁,本来不想过生日的,就想当做是平常的一天。结果我妹非要提醒我,大家伙瞧瞧,这像妹妹该说的话吗? -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-5addb157-141f-400b-a51f-77557c8fdb8d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-5addb157-141f-400b-a51f-77557c8fdb8d.png) 呜呜呜~ @@ -23,7 +23,7 @@ tag: **经营了近一年的知乎,阅读总数突破了一千万,这也是我人生当中的第一个**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-0324afde-4009-4e80-b878-2311ff88e5ca.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-0324afde-4009-4e80-b878-2311ff88e5ca.png) 其实早在 11 月就破了千万,当时就想记录一下,但细想一下,好像这点成绩也算不上什么。毕竟逼乎上人均 985、年薪百万、刚下飞机的大佬多的是。 @@ -33,13 +33,13 @@ tag: 这不,前几天一个帖子莫名其妙被知乎删除了,我是无感知的。一个小伙伴为了看这个帖子,还特意发起了一次 9.8 元的付费咨询。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-2fdd5b2b-67c5-40cf-b0e4-0a92a37e659a.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-8b4637f2-08c9-479b-855f-3fd332d44651.png) 不对啊,收藏竟然是点赞的 3 倍还多。。嗯,此时此刻天空飘出来了四个字:白票真香。 @@ -101,7 +101,7 @@ tag: 我在知乎上还有不少硬核输出,尤其是这些千赞以上的帖子,真心推荐给大家看看,看完后绝壁是有收获的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-4612a83f-6207-496c-b32b-c6f1ab031c4f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-4612a83f-6207-496c-b32b-c6f1ab031c4f.png) 虽然有些埋没的帖子我觉得价值也很高。不过,埋没就埋没吧。 diff --git a/docs/array/array.md b/docs/array/array.md new file mode 100644 index 0000000000..6200173faa --- /dev/null +++ b/docs/array/array.md @@ -0,0 +1,262 @@ +--- +title: 浅入浅出Java数组 +shortTitle: Java数组 +category: + - Java核心 +tag: + - 数组&字符串 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java数组 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java数组,数组 +--- + +“二哥,我看你公众号的一篇文章里提到,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 关键字,[基本数据类型](https://tobebetterjavaer.com/basic-grammar/basic-data-type.html)是不用的。然后,我们需要在方括号中指定数组的长度。 + +这时候,数组中的每个元素都会被初始化为默认值,int 类型的就为 0,Object 类型的就为 null。 不同数据类型的默认值不同,可以参照[之前的文章](https://tobebetterjavaer.com/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` 结束,不要使用超出这个范围内的索引访问数组,就不会抛出数组越界的异常了。 + +当数组的元素非常多的时候,逐个访问数组就太辛苦了,所以需要通过遍历的方式。 + +第一种,使用 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); +``` + +“除了一维数组,还有二维数组,三妹你可以去研究下,比如说用二维数组打印一下杨辉三角。”说完,我就去阳台上休息了,留三妹在那里学习,不能打扰她。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/array/print.md b/docs/array/print.md new file mode 100644 index 0000000000..d68130e9f7 --- /dev/null +++ b/docs/array/print.md @@ -0,0 +1,173 @@ +--- +title: 如何优雅地打印Java数组? +shortTitle: 打印Java数组 +category: + - Java核心 +tag: + - 数组&字符串 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,如何优雅地打印Java数组? +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java数组,打印数组 +--- + +“哥,[之前听你说,数组也是一个对象](https://tobebetterjavaer.com/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 流](https://tobebetterjavaer.com/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://tobebetterjavaer.com/java8/Lambda.html)。 + +“当然了,也可以使用比较土的方式,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://tobebetterjavaer.com/common-tool/arrays.html)在讲 Arrays 工具类的时候,提到过另外一种方法 `Arrays.toString()` 吗?”三妹看我一直说不到点子上,有点着急了。 + +“当然没有了,我认为 `Arrays.toString()` 是打印数组的最佳方式,没有之一。”我的情绪有点激动。 + +`Arrays.toString()` 可以将任意类型的数组转成字符串,包括基本类型数组和引用类型数组。该方法有多种重载形式。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/array/print-01.png) + +使用 `Arrays.toString()` 方法来打印数组再优雅不过了,就像,就像,就像蒙娜丽莎的微笑。 + +![](http://cdn.tobebetterjavaer.com/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 开发手册上有这样一条规约,你看。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/array/print-03.png) + +“什么是 POJO 呢,就是 Plain Ordinary Java Object 的缩写,一般在 Web 应用程序中建立一个数据库的映射对象时,我们称它为 POJO,这类对象不继承或不实现任何其它 Java 框架的类或接口。” + +“对于这样的类,最好是重写一下它的 `toString()` 方法,方便查看这个对象到底包含了什么字段,好排查问题。” + +“如果不重写的话,打印出来的 Java 对象就像直接打印数组的那样,一串谁也看不懂的字符序列。” + +“可以借助 Intellij IDEA 生成重写的 `toString()` 方法,特别方便。” + +“好的,哥,我记住了。以后遇到的话,我注意下。你去休息吧,我来敲一下你提到的这些代码,练一练。” + +“OK,我走,我走。” + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/48-keywords.md b/docs/basic-extra-meal/48-keywords.md new file mode 100644 index 0000000000..e52c2d69cc --- /dev/null +++ b/docs/basic-extra-meal/48-keywords.md @@ -0,0 +1,135 @@ +--- +title: Java中常用的48个关键字和2个保留字 +shortTitle: 48个关键字和2个保留字 +category: + - Java核心 +tag: + - Java语法基础 +description: Java程序员进阶之路,小白的零基础Java教程,Java中常用的48个关键字和2个保留字 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java保留字,Java关键字,关键字,保留字 +--- + +“二哥,就我之前学过的这些 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 等。”转动了一下僵硬的脖子后,我对三妹说。 + +“除了这些关键字,Java 中还有两个非常特殊的保留字(goto 和 const),它们不能在程序中使用。” + +“goto 在 C语言中叫做‘无限跳转’语句,在 Java 中,不再使用 goto 语句,因为无限跳转会破坏程序结构。” + +“const 在 C语言中是声明常量的关键字,在 Java 中可以使用 public static final 三个关键字的组合来达到常量的效果。” + +“好的二哥,我了解了,您辛苦了。” + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/src/basic-extra-meal/Overriding.md b/docs/basic-extra-meal/Overriding.md similarity index 87% rename from docs/src/basic-extra-meal/Overriding.md rename to docs/basic-extra-meal/Overriding.md index 3909563477..38cb13feff 100644 --- a/docs/src/basic-extra-meal/Overriding.md +++ b/docs/basic-extra-meal/Overriding.md @@ -5,11 +5,11 @@ category: - Java核心 tag: - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java重写(Overriding)时应当遵守的11条规则 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java重写(Overriding)时应当遵守的11条规则 head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,重写,Overriding + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,重写,Overriding --- @@ -19,7 +19,7 @@ head: 重写带来了一种非常重要的能力,可以让子类重新实现从超类那继承过来的方法。在下面这幅图中,Animal 是父类,Dog 是子类,Dog 重新实现了 `move()` 方法用来和父类进行区分,毕竟狗狗跑起来还是比较有特色的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-1.png) 重写的方法和被重写的方法,不仅方法名相同,参数也相同,只不过,方法体有所不同。 @@ -83,7 +83,7 @@ public class Animal { 由于父类 Animal 中的 `move()` 是 final 的,所以子类在尝试重写该方法的时候就出现编译错误了! -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-2.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-2.png) 同样的,如果一个方法是 static 的,也不允许重写,因为静态方法可用于父类以及子类的所有实例。 @@ -95,7 +95,7 @@ public class Animal { 重写的目的在于根据对象的类型不同而表现出多态,而静态方法不需要创建对象就可以使用。没有了对象,重写所需要的“对象的类型”也就没有存在的意义了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-3.png) ## 04、重写方法的要求 @@ -147,7 +147,7 @@ public class Dog extends Animal { 于是就编译出错了(返回类型不兼容)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-4.png) ### **规则五:重写的方法不能使用限制等级更严格的权限修饰符**。 @@ -175,7 +175,7 @@ public class Dog extends Animal { 如果子类中的方法用了更严格的权限修饰符,编译器就报错了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-5.png) ### **规则六:重写后的方法不能抛出比父类中更高级别的异常**。 @@ -326,9 +326,8 @@ synchronized 关键字用于在多线程环境中获取和释放监听对象, ---- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/basic-extra-meal/annotation.md b/docs/basic-extra-meal/annotation.md similarity index 84% rename from docs/src/basic-extra-meal/annotation.md rename to docs/basic-extra-meal/annotation.md index 829a06f6d1..43ede9aa2d 100644 --- a/docs/src/basic-extra-meal/annotation.md +++ b/docs/basic-extra-meal/annotation.md @@ -1,24 +1,24 @@ --- -title: Java注解,请别小看我。 -shortTitle: Java注解 +title: 深入理解Java中的注解 +shortTitle: 深入理解Java中的注解 category: - Java核心 tag: - Java重要知识点 -description: 本文深入探讨了Java注解的概念、分类及其在实际项目中的应用。通过详细的示例和解释,帮助读者更好地理解和掌握Java注解技术,学会如何自定义注解以及在实际开发中灵活运用,提升代码的可读性和可维护性。 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,深入理解Java中的注解 head: - - meta - name: keywords - content: Java,注解,annotation,java 注解,java annotation + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/annotation/annotation-01.png) “好吧,哥来告诉你吧。” @@ -37,7 +37,7 @@ public class AutowiredTest { } ``` -注意到 `@Autowired` 这个注解了吧?它本来是为 Spring(后面会讲)容器注入 Bean 的,现在被我无情地扔在了字段 name 的身上,但这段代码所在的项目中并没有启用 Spring,意味着 `@Autowired` 注解此时只是一个摆设。 +注意到 `@Autowired` 这个注解了吧?它本来是为 Spring 容器注入 Bean 的,现在被我无情地扔在了字段 name 的身上,但这段代码所在的项目中并没有启用 Spring,意味着 `@Autowired` 注解此时只是一个摆设。 “既然只是个摆设,那你这个地方为什么还要用 `@Autowired` 呢?”三妹好奇地问。 @@ -232,9 +232,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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/box.md b/docs/basic-extra-meal/box.md new file mode 100644 index 0000000000..e748c4ca63 --- /dev/null +++ b/docs/basic-extra-meal/box.md @@ -0,0 +1,273 @@ +--- +title: 深入剖析Java中的拆箱和装箱 +shortTitle: 深入剖析Java中的拆箱和装箱 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,深入剖析Java中的拆箱和装箱 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,装箱,拆箱,包装类型 +--- + + +“哥,听说 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` 画起了图。 + +![](http://cdn.tobebetterjavaer.com/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 毫秒。” + +“好了,三妹,今天的主题就先讲到这吧。我再去来根华子。” + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/class-object.md b/docs/basic-extra-meal/class-object.md similarity index 85% rename from docs/src/basic-extra-meal/class-object.md rename to docs/basic-extra-meal/class-object.md index b49fcb7627..4691d9d640 100644 --- a/docs/src/basic-extra-meal/class-object.md +++ b/docs/basic-extra-meal/class-object.md @@ -5,11 +5,11 @@ category: - Java核心 tag: - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java 中,先有Class还是先有Object? +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java 中,先有Class还是先有Object? head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,class,object + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,class,object --- @@ -21,7 +21,7 @@ Java 对象模型中: 那到底是先有Class还是先有Object? JVM 是怎么处理这个“鸡·蛋”问题呢? -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/class-object-2f47490c-70b8-41b8-9551-42c2f98eea91.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/class-object-2f47490c-70b8-41b8-9551-42c2f98eea91.png) 针对这个问题,我在知乎上看到了 R 大的一个回答,正好解答了我心中的疑惑,就分享出来给各位小伙伴一个参考和启发~ @@ -94,12 +94,11 @@ http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/ade5be2b1758/src/share/vm/cl ---- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/comparable-omparator.md b/docs/basic-extra-meal/comparable-omparator.md new file mode 100644 index 0000000000..ad82eade98 --- /dev/null +++ b/docs/basic-extra-meal/comparable-omparator.md @@ -0,0 +1,184 @@ +--- +title: 详解Java中Comparable和Comparator接口的区别 +shortTitle: Comparable和Comparator的区别 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Comparable和Comparator接口的区别 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,泛型 +--- + +那天,小二去马蜂窝面试,面试官老王一上来就甩给了他一道面试题:请问Comparable和Comparator有什么区别?小二差点笑出声,因为三年前,也就是 2021 年,他在《[Java 程序员进阶之路](https://tobebetterjavaer.com/)》专栏上看到过这题😆。 + +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`。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/deep-copy.md b/docs/basic-extra-meal/deep-copy.md similarity index 84% rename from docs/src/basic-extra-meal/deep-copy.md rename to docs/basic-extra-meal/deep-copy.md index f5e662fa53..290b5133bb 100644 --- a/docs/src/basic-extra-meal/deep-copy.md +++ b/docs/basic-extra-meal/deep-copy.md @@ -1,31 +1,29 @@ --- -title: 深入理解Java浅拷贝与深拷贝 -shortTitle: 深入理解Java浅拷贝与深拷贝 +title: 一文彻底讲明白的Java中的浅拷贝与深拷贝 +shortTitle: Java中的浅拷贝与深拷贝 category: - Java核心 tag: - Java重要知识点 -description: 本文详细讨论了Java中的浅拷贝和深拷贝概念,解析了它们如何在实际编程中应用。文章通过实例演示了如何实现浅拷贝与深拷贝,以帮助读者更好地理解这两种拷贝方式在Java编程中的作用与应用场景。 -author: 沉默王二 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,一文彻底讲明白的Java中的浅拷贝与深拷贝 head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,深拷贝,浅拷贝 + content: Java,Java SE,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++ 代码,进而调用操作系统去完成对象的克隆工作。 “哥,那你就先说浅拷贝吧!” @@ -50,6 +48,7 @@ class Writer implements Cloneable{ Writer 类有两个字段,分别是 int 类型的 age,和 String 类型的 name。然后重写了 `toString()` 方法,方便打印对象的具体信息。 + “为什么要实现 Cloneable 接口呢?”三妹开启了十万个为什么的模式。 Cloneable 接口是一个标记接口,它肚子里面是空的: @@ -109,7 +108,7 @@ writer2:Writer@b97c004{age=18, name='三妹'} 可以看得出,浅拷贝后,writer1 和 writer2 引用了不同的对象,但值是相同的,说明拷贝成功。之后,修改了 writer2 的 name 字段,直接上图就明白了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/deep-copy-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/deep-copy-01.png) 之前的例子中,Writer 类只有两个字段,没有引用类型字段。那么,我们再来看另外一个例子,为 Writer 类增加一个自定义的引用类型字段 Book,先来看 Book 的定义。 @@ -208,7 +207,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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/deep-copy-02.png) “哇,哥,果真一图胜千言,我明白了。”三妹似乎对我画的图很感兴趣呢,“那你继续说深拷贝吧!” @@ -309,15 +308,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) +![](http://cdn.tobebetterjavaer.com/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 接口类似,都是标记型接口。” 来看例子。 @@ -424,9 +423,8 @@ 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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/enum.md b/docs/basic-extra-meal/enum.md new file mode 100644 index 0000000000..e8cbe60fff --- /dev/null +++ b/docs/basic-extra-meal/enum.md @@ -0,0 +1,303 @@ +--- +title: 新来个技术总监,彻底把 Java 枚举(enum)讲清楚了 +shortTitle: Java枚举(enum) +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,新来个技术总监,彻底把 Java 枚举(enum)讲清楚了 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,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()` 方法则不会。 + +![](http://cdn.tobebetterjavaer.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 接口(后面会讲)的实现类,它是处理枚举类型数据的一把利器,非常高效。”我说,“从名字上就可以看得出,EnumSet 不仅和 Set 有关系,和枚举也有关系。” + +“因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法。” + + +![](http://cdn.tobebetterjavaer.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 的一些方法了,见下图。 + +![](http://cdn.tobebetterjavaer.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 的一些方法了,见下图。 + +![](http://cdn.tobebetterjavaer.com/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基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/equals-hashcode.md b/docs/basic-extra-meal/equals-hashcode.md new file mode 100644 index 0000000000..ab4981277d --- /dev/null +++ b/docs/basic-extra-meal/equals-hashcode.md @@ -0,0 +1,245 @@ +--- +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,hashcode,equals +--- + +“二哥,我在读《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 并不相同。” + +![](http://cdn.tobebetterjavaer.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。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/equals-hashcode-02.png) + + +“每当重写 equals 方法时,hashCode 方法也需要重写,原因就是为了保证:如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同。”我点题了。 + +“OK,get 了。”三妹开心地点了点头,看得出来,今天学到了不少。 + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/fanshe.md b/docs/basic-extra-meal/fanshe.md new file mode 100644 index 0000000000..ff32670f46 --- /dev/null +++ b/docs/basic-extra-meal/fanshe.md @@ -0,0 +1,335 @@ +--- +title: 大白话说清楚Java反射:入门、使用、原理 +shortTitle: Java反射:入门、使用、原理 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,大白话说清楚Java反射:入门、使用、原理 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,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"); +``` + +第二步,通过 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 接口来完成的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/fanshe/fanshe-01.png) + +MethodAccessor 接口有三个实现类,其中的 MethodAccessorImpl 是一个抽象类,另外两个具体的实现类继承了这个抽象类。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/fanshe/fanshe-02.png) + +- NativeMethodAccessorImpl:通过本地方法来实现反射调用; +- DelegatingMethodAccessorImpl:通过委派模式来实现反射调用; + +通过 debug 的方式进入 `invoke()` 方法后,可以看到第一次反射调用会生成一个委派实现 DelegatingMethodAccessorImpl,它在生成的时候会传递一个本地实现 NativeMethodAccessorImpl。 + +![](http://cdn.tobebetterjavaer.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。 + +![](http://cdn.tobebetterjavaer.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 +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](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基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/basic-extra-meal/generic.md b/docs/basic-extra-meal/generic.md new file mode 100644 index 0000000000..d30b1c7dbb --- /dev/null +++ b/docs/basic-extra-meal/generic.md @@ -0,0 +1,479 @@ +--- +title: 深入理解Java中的泛型 +shortTitle: 深入理解Java中的泛型 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,深入理解Java中的泛型 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,泛型 +--- + +“二哥,为什么要设计泛型啊?”三妹开门见山地问。 + +“三妹啊,听哥慢慢给你讲啊。”我说。 + +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()); + } +} +``` + +不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。 + +![](http://cdn.tobebetterjavaer.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` 可以缩小泛型的类型范围。 + +“哦,明白了。”三妹若有所思的点点头,“二哥,听说虚拟机没有泛型?” + +“三妹,你功课做得可以啊。哥可以肯定地回答你,虚拟机是没有泛型的。” + +“怎么确定虚拟机有没有泛型呢?”三妹问。 + +“只要我们把泛型类的字节码进行反编译就看到了!”用反编译工具将 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](https://www.pdai.tech/md/java/basic/java-basic-x-generic.html) + +“对泛型机制讲的也很透彻,你结合二哥给你讲的这些,再深入的学习一下。” + +“好的,二哥。” + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/hashcode.md b/docs/basic-extra-meal/hashcode.md new file mode 100644 index 0000000000..84f8473210 --- /dev/null +++ b/docs/basic-extra-meal/hashcode.md @@ -0,0 +1,247 @@ +--- +title: 深入理解Java中的hashCode方法 +shortTitle: 深入理解Java中的hashCode方法 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,深入理解Java中的hashCode方法 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,hashcode +--- + + + +假期结束了,需要快速切换到工作的状态投入到新的一天当中。放假的时候痛快地玩耍,上班的时候积极的工作,这应该是我们大多数“现代人”该有的生活状态。 + +我之所以费尽心思铺垫了前面这段话,就是想告诉大家,技术文虽迟但到,来吧,学起来~ + +今天我们来谈谈 Java 中的 `hashCode()` 方法。众所周知,Java 是一门面向对象的编程语言,所有的类都会默认继承自 Object 类,而 Object 的中文意思就是“对象”。 + +Object 类中就包含了 `hashCode()` 方法: + +```java +@HotSpotIntrinsicCandidate +public native int hashCode(); +``` + +意味着所有的类都会有一个 `hashCode()` 方法,该方法会返回一个 int 类型的值。由于 `hashCode()` 方法是一个本地方法(`native` 关键字修饰的方法,用 `C/C++` 语言实现,由 Java 调用),意味着 Object 类中并没有给出具体的实现。 + +具体的实现可以参考 `jdk/src/hotspot/share/runtime/synchronizer.cpp`(源码可以到 GitHub 上 OpenJDK 的仓库中下载)。`get_next_hash()` 方法会根据 hashCode 的取值来决定采用哪一种哈希值的生成策略。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/hashcode-1.png) + +并且 `hashCode()` 方法被 `@HotSpotIntrinsicCandidate` 注解修饰,说明它在 HotSpot 虚拟机中有一套高效的实现,基于 CPU 指令。 + +那大家有没有想过这样一个问题:为什么 Object 类需要一个 `hashCode()` 方法呢? + +在 Java 中,`hashCode()` 方法的主要作用就是为了配合哈希表使用的。 + +哈希表(Hash Table),也叫散列表,是一种可以通过关键码值(key-value)直接访问的数据结构,它最大的特点就是可以快速实现查找、插入和删除。其中用到的算法叫做哈希,就是把任意长度的输入,变换成固定长度的输出,该输出就是哈希值。像 MD5、SHA1 都用的是哈希算法。 + +像 Java 中的 HashSet、Hashtable(注意是小写的 t)、HashMap 都是基于哈希表的具体实现。其中的 HashMap 就是最典型的代表,不仅面试官经常问,工作中的使用频率也非常的高。 + +大家想一下,如果没有哈希表,但又需要这样一个数据结构,它里面存放的数据是不允许重复的,该怎么办呢? + +要不使用 `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) { + hash = h = isLatin1() ? StringLatin1.hashCode(value) + : StringUTF16.hashCode(value); + } + return h; +} +public static int hashCode(byte[] value) { + int h = 0; + int length = value.length >> 1; + for (int i = 0; i < length; i++) { + h = 31 * h + getChar(value, i); + } + return h; +} +``` + +可想而知,经过这么一系列复杂的运算,再加上 JDK 作者这种大师级别的设计,哈希冲突的概率我相信已经降到了最低。 + +当然了,从理论上来说,对于两个不同对象,它们通过 `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 类的 `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 为字符串长度)。 + +![](http://cdn.tobebetterjavaer.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++ 源码。 + +```java +static inline intptr_t get_next_hash(Thread* current, oop obj) { + intptr_t value = 0; + if (hashCode == 0) { + // This form uses global Park-Miller RNG. + // On MP system we'll have lots of RW access to a global, so the + // mechanism induces lots of coherency traffic. + value = os::random(); + } else if (hashCode == 1) { + // This variation has the property of being stable (idempotent) + // between STW operations. This can be useful in some of the 1-0 + // synchronization schemes. + intptr_t addr_bits = cast_from_oop(obj) >> 3; + value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; + } else if (hashCode == 2) { + value = 1; // for sensitivity testing + } else if (hashCode == 3) { + value = ++GVars.hc_sequence; + } else if (hashCode == 4) { + value = cast_from_oop(obj); + } else { + // Marsaglia's xor-shift scheme with thread-specific state + // This is probably the best overall implementation -- we'll + // likely make this the default in future releases. + 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基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/immutable.md b/docs/basic-extra-meal/immutable.md new file mode 100644 index 0000000000..d186d9a8fd --- /dev/null +++ b/docs/basic-extra-meal/immutable.md @@ -0,0 +1,217 @@ +--- +title: 深入理解Java中的不可变对象 +shortTitle: 深入理解Java中的不可变对象 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,深入理解Java中的不可变对象 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,不可变对象,immutable +--- + +# 深入理解Java中的不可变对象 + +>二哥,你能给我说说为什么 String 是 immutable 类(不可变对象)吗?我想研究它,想知道为什么它就不可变了,这种强烈的愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。二哥你的文章总是充满趣味性,我想一定能够说明白,我也一定能够看明白,能在接下来写一写吗? + +收到读者小 R 的私信后,我就总感觉自己有一种义不容辞的责任,非要把 immutable 类说明白不可! + + +## 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 类那样,尤其是在多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗,但这个缺点相比它带来的优点,显然是微不足道的——无非就是捡了西瓜,丢了芝麻。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/instanceof-jvm.md b/docs/basic-extra-meal/instanceof-jvm.md similarity index 80% rename from docs/src/basic-extra-meal/instanceof-jvm.md rename to docs/basic-extra-meal/instanceof-jvm.md index fcc703b8dc..3084de7ab1 100644 --- a/docs/src/basic-extra-meal/instanceof-jvm.md +++ b/docs/basic-extra-meal/instanceof-jvm.md @@ -5,11 +5,11 @@ category: - Java核心 tag: - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java中的instanceof关键字是如何实现的? +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java中的instanceof关键字是如何实现的? head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,instanceof + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,instanceof --- @@ -19,7 +19,7 @@ head: >作者: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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/instanceof-jvm-b676fee6-bfd4-4ae9-9c7b-e488e345f775.gif) -------- @@ -53,7 +53,7 @@ JVM有一条名为 instanceof 的指令,而Java源码编译到Class文件时 javac是这样做的: - instanceof 是javac能识别的一个关键字,对应到Token.INSTANCEOF的token类型。做词法分析的时候扫描到"instanceof"关键字就映射到了一个Token.INSTANCEOF token。 -- 该编译器的抽象语法树节点有一个JCTree.JCInstanceOf类用于表示instanceof运算。做语法分析的时候解析到[instanceof运算符](https://javabetter.cn/basic-extra-meal/instanceof.html)就会生成这个JCTree.JCInstanceof类型的节点。 +- 该编译器的抽象语法树节点有一个JCTree.JCInstanceOf类用于表示instanceof运算。做语法分析的时候解析到[instanceof运算符](https://tobebetterjavaer.com/basic-extra-meal/instanceof.html)就会生成这个JCTree.JCInstanceof类型的节点。 - 中途还得根据Java语言规范对instanceof运算符的编译时检查的规定把有问题的情况找出来。 - 到最后生成字节码的时候为JCTree.JCInstanceof节点生成instanceof字节码指令。 @@ -115,9 +115,8 @@ VM runtime的: ---- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/instanceof.md b/docs/basic-extra-meal/instanceof.md new file mode 100644 index 0000000000..4f77ff3c3a --- /dev/null +++ b/docs/basic-extra-meal/instanceof.md @@ -0,0 +1,156 @@ +--- +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,instanceof +--- + + +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基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/int-cache.md b/docs/basic-extra-meal/int-cache.md new file mode 100644 index 0000000000..b8cf1b6824 --- /dev/null +++ b/docs/basic-extra-meal/int-cache.md @@ -0,0 +1,182 @@ +--- +title: Java中new Integer与Integer.valueOf的区别 +shortTitle: new Integer与Integer.valueOf的区别 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java中new Integer与Integer.valueOf的区别 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Integer +--- + +“三妹,今天我们来补一个小的知识点: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://tobebetterjavaer.com/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 是这样用的啊,我明白了。”三妹表示学会了。 + +“那,缓存池之所以存在的原因也是因为这样做可以提高程序的整体性能,因为相对来说,比如说 Integer,-128~127 这个范围内的 256 个数字使用的频率会高一点。”我总结道。 + +“get 了!二哥你真棒,又学到了。”三妹很开心~ + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/java-naming.md b/docs/basic-extra-meal/java-naming.md similarity index 83% rename from docs/src/basic-extra-meal/java-naming.md rename to docs/basic-extra-meal/java-naming.md index 10e6e3663a..171b206c2c 100644 --- a/docs/src/basic-extra-meal/java-naming.md +++ b/docs/basic-extra-meal/java-naming.md @@ -1,16 +1,15 @@ --- -title: 5 分钟编码,1 小时命名,笑 +title: Java命名规范,告别编码 5 分钟,命名 2 小时 shortTitle: Java命名规范 category: - Java核心 tag: - Java语法基础 -description: 本文介绍了Java编程中的命名规范,以帮助程序员编写可读性强、易于维护的代码。我们将从变量、方法、类和接口命名等方面讲解最佳实践,以便在项目中保持一致的代码风格。学习并实践这些命名规范,将使你成为更出色的Java程序员。 -author: 沉默王二 +description: Java程序员进阶之路,小白的零基础Java教程,Java命名规范,告别编码 5 分钟,命名 2 小时 head: - - meta - name: keywords - content: Java,Java命名规范, 变量命名, 方法命名, 类命名, 接口命名, 代码风格, 代码质量 + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java命名规范,命名规范 --- “二哥,Java 中的命名约定都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。 @@ -25,8 +24,6 @@ head: 拿我这个笔名“沉默王二”来举例吧,读起来我就觉得朗朗上口,读者看到这个笔名就知道我是一个什么样的人——对不熟的人保持沉默,对熟的人妙语连珠,哈哈。 ->当然了,如果你暂时记不住也没关系,后面再回头来记一下就好了。 - ### 01、包(package) 包的命名应该遵守以下规则: @@ -36,6 +33,7 @@ head: - 包名统一使用单数形式,比如说 `com.itwanger.util` 不能是 `com.itwanger.utils` - 在最新的 Java 编程规范中,要求开发人员在自己定义的包名前加上唯一的前缀。由于互联网上的域名是不会重复的,所以多数开发人员采用自己公司(或者个人博客)在互联网上的域名称作为包的唯一前缀。比如我文章中出现的代码示例的包名就是 `package com.itwanger`。 + ### 02、类(class) 类的命名应该遵守以下规则: @@ -45,7 +43,7 @@ head: - 类名使用 UpperCamelCase(驼峰式命名)风格 - 尽量不要省略成单词的首字母,但以下情形例外:DO/BO/DTO/VO/AO/PO/UID 等 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/fifteen-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/fifteen-01.png) 另外,如果是抽象类的话,使用 Abstract 或 Base 开头;如果是异常类的话,使用 Exception 结尾;如果是测试类的话,使用 Test 结尾。 @@ -117,6 +115,7 @@ class Son extends ConfusingName { static final int MIN_AGE = 18; ``` + ### 06、方法(method) 方法的命名应该遵守以下规则: @@ -157,9 +156,8 @@ 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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ 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..7518e2caa6 --- /dev/null +++ b/docs/basic-extra-meal/java-unicode.md @@ -0,0 +1,186 @@ +--- +title: 彻底弄懂Java中的Unicode和UTF-8编码 +shortTitle: 彻底弄懂Unicode和UTF-8编码 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,彻底弄懂Java中的Unicode和UTF-8编码 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,unicode,utf-8 +--- + +“二哥,[上一篇](https://tobebetterjavaer.com/basic-grammar/basic-data-type.html)文章中提到了 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 码表示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-01.png) + +PS:拉丁字母(也称为罗马字母)是多数欧洲语言采用的字母系统,是世界上最通行的字母文字系统,是罗马文明的成果之一。 + +虽然名称上叫作拉丁字母,但拉丁文中并没有用 J、U 和 W 这三个字母。 + +在我们的印象中,可能说拉丁字母多少有些陌生,说英语字母可能就有直观的印象了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-02.png) + +PPS:阿拉伯数字,我们都很熟悉了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-03.png) + +但是,阿拉伯数字并非起源于阿拉伯,而是起源于古印度。学过历史的我们应该有一些印象,阿拉伯分布于西亚和北非,以阿拉伯语为主要语言,以伊斯兰教为主要信仰。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-04.png) + +处在这样的地理位置,做起东亚和欧洲的一些生意就很有优势,于是阿拉伯数字就由阿拉伯人传到了欧洲,因此得名。 + +PPPS:英式标点符号,也叫英文标点符号,和中文标点符号很相近。标点符号是辅助文字记录语言的符号,是书面语的组成部分,用来表示停顿、加强语气等。 + +英文标点符号在 16 世纪时,分为朗诵学派和句法学派,主要由古典时期的希腊文和拉丁文演变而来,在 17 世纪后进入稳定阶段。俄文的标点符号依据希腊文而来,到了 18 世纪后也采用了英文标点符号。 + +在很多人的印象中,古文是没有标点符号的,但管锡华博士研究指出,中国早在先秦时代就有标点符号了,后来融合了一些英文标点符号后,逐渐形成了现在的中文标点符号。 + +![](http://cdn.tobebetterjavaer.com/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 个符号。 + +要知道,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-06.png) + +PPS:这“锟斤拷”价格挺公道的啊!!!(逃 + +如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会彻底消失。 + +这个艰巨的任务有谁来完成呢?**Unicode**,中文译作万国码、国际码、统一码、单一码,就像它的名字都表示的,这是一种所有符号的编码。 + +Unicode 至今仍在不断增修,每个新版本都会加入更多新的字符。目前最新的版本为 2020 年 3 月公布的 13.0,收录了 13 万个字符。 + +![](http://cdn.tobebetterjavaer.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.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 类型)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-08.png) + +也就是说,在 Java 中,char 会占用两个字节,超出 char 的承受范围('\u0000'(0)和 '\uffff'(65,535))的字符,都将无法表示。 + + + +“好了,三妹,关于 Unicode 就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md b/docs/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md similarity index 79% rename from docs/src/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md rename to docs/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md index 563238326d..9a0422a9f9 100644 --- a/docs/src/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md +++ b/docs/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md @@ -5,11 +5,11 @@ category: - Java核心 tag: - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)? +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)? head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,jdk,无限循环,for,while + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,jdk,无限循环,for,while --- @@ -109,9 +109,8 @@ public void bar() { ---- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/jdk9-char-byte-string.md b/docs/basic-extra-meal/jdk9-char-byte-string.md new file mode 100644 index 0000000000..64d0a58963 --- /dev/null +++ b/docs/basic-extra-meal/jdk9-char-byte-string.md @@ -0,0 +1,122 @@ +--- +title: jdk9为什么要将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,string,char,byte +--- + +大家好,我是二哥呀!如果你不是 Java8 的钉子户,你应该早就发现了:String 类的源码已经由 `char[]` 优化为了 `byte[]` 来存储字符串内容,为什么要这样做呢? + +开门见山地说,从 `char[]` 到 `byte[]`,最主要的目的是**为了节省字符串占用的内存**。内存占用减少带来的另外一个好处,就是 GC 次数也会减少。 + +## 一、为什么要优化 String 节省内存空间 + +我们使用 `jmap -histo:live pid | head -n 10` 命令就可以查看到堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。 + +以我正在运行着的编程喵喵项目实例(基于 Java 8)来说,结果是这样的。 + +![](http://cdn.tobebetterjavaer.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 这么高的类库,就显得非常的鸡肋。 + +## 二、`byte[]` 为什么就能节省内存空间呢? + +众所周知,char 类型的数据在 JVM 中是占用两个字节的,并且使用的是 UTF-8 编码,其值范围在 '\u0000'(0)和 '\uffff'(65,535)(包含)之间。 + + + +也就是说,使用 `char[]` 来表示 String 就导致了即使 String 中的字符只用一个字节就能表示,也得占用两个字节。 + +而实际开发中,单字节的字符使用频率仍然要高于双字节的。 + +当然了,仅仅将 `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)。 + +关于字符编码,我在《Java 程序员进阶之路》里曾讲到过,想要深入了解的小伙伴查看下面的链接🔗: + +>https://tobebetterjavaer.com/basic-extra-meal/java-unicode.html + +也就是说,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基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/override-overload.md b/docs/basic-extra-meal/override-overload.md new file mode 100644 index 0000000000..8570bd979d --- /dev/null +++ b/docs/basic-extra-meal/override-overload.md @@ -0,0 +1,326 @@ +--- +title: Java 方法重写 Override 和方法重载 Overload 的区别,一下子就明白了 +shortTitle: Java 方法重写和方法重载的区别 +description: Java程序员进阶之路,小白的零基础Java教程,Java 中方法重写 Override 和方法重载 Overload 的区别 +category: + - Java 核心 +tag: + - 面向对象编程 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,方法重写,方法重载,Override,Overload +--- + +## 01、开篇 + +入冬的夜,总是来得特别的早。我静静地站在阳台,目光所及之处,不过是若隐若现的钢筋混凝土,还有那毫无情调的灯光。 + +“哥,别站在那发呆了。今天学啥啊,七点半我就要回学校了,留给你的时间不多了,你要抓紧哦。”三妹傲娇的声音一下子把我从游离的状态拉回到了现实。 + +“今天要学习 Java 中的方法重载与方法重写。”我迅速地走到电脑前面,打开一份 Excel 文档,看了一下《教妹学 Java》的进度,然后对三妹说。 + +“如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。 ”我面带着朴实无华的微笑继续说,“如果方法的功能是一样的,但参数不同,使用相同的名字可以提高程序的可读性。” + +“如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体可能不同),我们称之为方法重写。 方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件。” + +“只不过,方法重载与方法重写在名字上很相似,就像是兄弟俩,导致初学者经常把它们俩搞混。” + +“方法重载的英文名叫 Overloading,方法重写的英文名叫 Overriding,因此,不仅中文名很相近,英文名之间也很相近,这就更容易让初学者搞混了。” + +“但两者其实是完全不同的!通过下面这张图,你就能看得一清二楚。” + +话音刚落,我就在 IDEA 中噼里啪啦地敲了起来。两段代码,分别是方法重写和方法重载。然后,把这两段代码截图到 draw.io(一个很漂亮的在线画图网站)上,加了一些文字说明。最后,打开 Photoscape X,把两张图片合并到了一起。 + +![](http://cdn.tobebetterjavaer.com/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。” + +“二哥,改变参数的数目和类型都可以实现方法重载,为什么改变方法的返回值类型就不可以呢?”三妹很能抓住问题的重点嘛。 + +“因为仅仅改变返回值类型的话,会把编译器搞懵逼的。”我略带调皮的口吻回答她。 + +“编译时报错优于运行时报错,所以当两个方法的名字相同,参数个数和类型也相同的时候,虽然返回值类型不同,但依然会提示方法已经被定义的错误。” + +![](http://cdn.tobebetterjavaer.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.tobebetterjavaer.com/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 方法的时候,编译器会提示错误。” +![](http://cdn.tobebetterjavaer.com/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 分钟了。 + +“首先来说一下方法重载时的注意事项,‘两同一不同’。” + +“‘两同’:在同一个类,方法名相同。” + +“‘一不同’:参数不同。” + +“再来说一下方法重写时的注意事项,‘两同一小一大’。” + +“‘两同’:方法名相同,参数相同。” + +“‘一小’:子类方法声明的异常类型要比父类小一些或者相等。” + +“‘一大’:子类方法的访问权限应该比父类的更大或者相等。” + +“记住了吧?三妹。带上口罩,拿好手机,咱准备出门吧。”今天限号,没法开车送三妹去学校了。 + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/pass-by-value.md b/docs/basic-extra-meal/pass-by-value.md new file mode 100644 index 0000000000..7c8e5af0d6 --- /dev/null +++ b/docs/basic-extra-meal/pass-by-value.md @@ -0,0 +1,152 @@ +--- +title: Java到底是值传递还是引用传递? +shortTitle: Java到底是值传递还是引用传递? +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java到底是值传递还是引用传递? +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,值传递,引用传递 +--- + + +“哥,说说 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 是引用类型,变量中保存的是对象的地址。一般称这种变量为对象的引用,引用存放在栈中,而对象存放在堆中。 + +这里说的栈和堆,是指内存中的一块区域,和数据结构中的栈和堆不一样。栈是由编译器自动分配释放的,所以适合存放编译期就确定生命周期的数据;而堆中存放的数据,编译器是不需要知道生命周期的,创建后的回收工作由垃圾收集器来完成。 + +“画幅图。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/pass-by-value-01.png) + +当用 = 赋值运算符改变 age 和 name 的值时。 + +```java +age = 16; +name = "三妹"; +``` + +对于基本类型 age,赋值运算符会直接改变变量的值,原来的值被覆盖。 + +对于引用类型 name,赋值运算符会改变对象引用中保存的地址,原来的地址被覆盖,但原来的对象不会被覆盖。 + +![](http://cdn.tobebetterjavaer.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 的地址,指向的是堆中“二哥”的位置。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/pass-by-value-03.png) + +当 `modify()` 方法调用结束后,改变了形参 name1 的地址,但 `main()` 方法中 name 并没有发生改变。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/pass-by-value-04.png) + +总结: + +- Java 中的参数传递是按值传递的。 +- 如果参数是基本类型,传递的是基本类型的字面量值的拷贝。 +- 如果参数是引用类型,传递的是引用的对象在堆中地址的拷贝。 + +“好了,三妹,今天的学习就到这吧。” + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/src/basic-extra-meal/true-generic.md b/docs/basic-extra-meal/true-generic.md similarity index 81% rename from docs/src/basic-extra-meal/true-generic.md rename to docs/basic-extra-meal/true-generic.md index 0b3356dd3a..566872e321 100644 --- a/docs/src/basic-extra-meal/true-generic.md +++ b/docs/basic-extra-meal/true-generic.md @@ -1,22 +1,21 @@ --- -title: Java 为什么无法实现真正的泛型? -shortTitle: Java为什么无法实现真正的泛型 +title: Java不能实现真正泛型的原因是什么? +shortTitle: Java不能实现真正泛型的原因是什么? category: - Java核心 tag: - Java重要知识点 -description: Java 无法实现真正泛型的原因在于类型擦除,这种设计是为了兼容早期 Java 版本。本文详细探讨 Java 泛型的实现机制、类型擦除背后的原理,以及 Java 泛型在编程中的局限性。 -author: 沉默王二 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java不能实现真正泛型的原因是什么? head: - - meta - name: keywords - content: Java, 泛型, 类型擦除 + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,泛型 --- -“二哥,为啥 Java 不能实现真正的泛型啊?”三妹开门见山地问。 +“二哥,为啥 Java 不能实现真正泛型啊?”三妹开门见山地问。 -简单来回顾一下[类型擦除](https://javabetter.cn/basic-extra-meal/generic.html),看下面这段代码。 +简单来回顾一下类型擦除,看下面这段代码。 ```java public class Cmower { @@ -58,7 +57,8 @@ public class Cmower { “保持耐心,好不好?”我安慰道。 -**第一,兼容性** + +第一,兼容性 Java 在 2004 年已经积累了较为丰富的生态,如果把现有的类修改为泛型类,需要让所有的用户重新修改源代码并且编译,这就会导致 Java 1.4 之前打下的江山可能会完全覆灭。 @@ -68,7 +68,7 @@ Java 在 2004 年已经积累了较为丰富的生态,如果把现有的类修 老用户不受影响,新用户可以自由地选择使用泛型,可谓一举两得。 -**第二,不是“实现不了”**。Pizza,1996 年的实验语言,在 Java 的基础上扩展了泛型。 +第二,不是“实现不了”。Pizza,1996 年的实验语言,在 Java 的基础上扩展了泛型。 >Pizza 教程地址:[http://pizzacompiler.sourceforge.net/doc/tutorial.html](http://pizzacompiler.sourceforge.net/doc/tutorial.html) @@ -120,7 +120,7 @@ String s = a.get(); 对吧?这就是我们想要的“真正意义上的泛型”,A 不仅仅可以是引用类型 String,还可以是基本数据类型。要知道,Java 的泛型不允许是基本数据类型,只能是包装器类型。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/true-generic-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/generic/true-generic-01.png) 除此之外,Pizza 的泛型还可以直接使用 `new` 关键字进行声明,并且 Pizza 编译器会从构造方法的参数上推断出具体的对象类型,究竟是 String 还是 int。要知道,Java 的泛型因为类型擦除的原因,程序员是无法知道一个 ArrayList 究竟是 `ArrayList` 还是 `ArrayList` 的。 @@ -187,11 +187,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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/generic/true-generic-02.png) 针对泛型,兼容性具体表现在什么地方呢?来看下面这段代码。 @@ -220,7 +220,7 @@ Java 神奇就神奇在这,表面上万物皆对象,但为了性能上的考 一个好消息是 Valhalla 项目正在努力解决这些因为泛型擦除带来的历史遗留问题。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/true-generic-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/generic/true-generic-03.png) Project Valhalla:正在进行当中的 OpenJDK 项目,计划给未来的 Java 添加改进的泛型支持。 @@ -235,9 +235,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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ 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..5de1762737 --- /dev/null +++ b/docs/basic-extra-meal/varables.md @@ -0,0 +1,157 @@ +--- +title: 详解Java中可变参数的使用 +shortTitle: 详解Java中可变参数的使用 +category: + - Java核心 +tag: + - Java重要知识点 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,详解Java中可变参数的使用 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,可变参数 +--- + +# 详解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()` 方法。 + +说到可变参数,我想起来阿里巴巴开发手册上有这样一条规约。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/varables-01.png) + +意思就是尽量不要使用可变参数,如果要用的话,可变参数必须要在参数列表的最后一位。既然坑位有限,只能在最后,那么可变参数就只能有一个(悠着点,悠着点)。如果可变参数不在最后一位,IDE 就会提示对应的错误,如下图所示。 + +![](http://cdn.tobebetterjavaer.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.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)`,傻傻分不清。 + +![](http://cdn.tobebetterjavaer.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()` 方法的内部要做好判空操作。 + +--- + + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-grammar/basic-data-type.md b/docs/basic-grammar/basic-data-type.md new file mode 100644 index 0000000000..6240a64baa --- /dev/null +++ b/docs/basic-grammar/basic-data-type.md @@ -0,0 +1,406 @@ +--- +title: Java 中的数据类型(8 种基本数据类型和引用数据类型) +shortTitle: Java 中的数据类型 +category: + - Java核心 +tag: + - Java语法基础 +description: Java程序员进阶之路,小白的零基础Java教程,Java 中的数据类型(8 种基本数据类型和引用数据类型) +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java 简介,Java数据类型,基本数据类型,引用数据类型,数据类型 +--- + +## 数据类型的分类 + +“Java 是一种静态类型的编程语言,这意味着所有变量必须在使用之前声明好,也就是必须得先指定变量的类型和名称。”我吸了一口麦香可可奶茶后对三妹说。 + +Java 中的数据类型可分为 2 种: + +1)**基本数据类型**。 + +基本数据类型是 Java 语言操作数据的基础,包括 boolean、char、byte、short、int、long、float 和 double,共 8 种。 + +2)**引用数据类型**。 + +除了基本数据类型以外的类型,都是所谓的引用类型。常见的有[数组](https://tobebetterjavaer.com/array/array.html)(对,没错,数组是引用类型)、class(也就是[类](https://tobebetterjavaer.com/oo/object-class.html)),以及[接口](https://tobebetterjavaer.com/oo/interface.html)(指向的是实现接口的类的对象)。 + +来个思维导图,感受下。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-01.png) + +[变量](https://tobebetterjavaer.com/oo/var.html)可以分为局部变量、成员变量、静态变量。 + +当变量是局部变量的时候,必须得先初始化,否则编译器不允许你使用它。拿 int 来举例吧,看下图。 + +![](http://cdn.tobebetterjavaer.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 | 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 和程序员的关系了吧?狗头保命) + +![](http://cdn.tobebetterjavaer.com/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 位。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-04.png) + +双精度是这样的格式,1 位符号,11 位指数,52 为小数,有效位数为 16 位。 + +![](http://cdn.tobebetterjavaer.com/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 字符集。字符集也可以叫编码,编码不同,实际占用的字节就会不同。” + +[关于字符编码](https://tobebetterjavaer.com/basic-extra-meal/java-unicode.html) + + +## 关于 int 和 char 类型互转 + +这里整理一波 int 和 char 类型的互转,它们之间比较特殊。也会在以后的学习当中经常遇到。 + +1)可以通过[强制类型转换](https://tobebetterjavaer.com/basic-grammar/type-cast.html)将整型 int 转换为字符 char。 + +```java +public class SimpleTesting { + public static void main(String[] args) { + int value_int = 65; + char value_char = (char) value_int; + System.out.println(value_char); + } +} +``` + +输出 `A`(其 [ASCII 值](https://tobebetterjavaer.com/basic-extra-meal/java-unicode.html)可以通过整数 65 来表示)。 + +2)可以使用 `Character.forDigit()` 方法将整型 int 转换为字符 char。 + +```java +public class SimpleTesting { + public static void main(String[] args) { + //radix 10 is for decimal number, for hexa use radix 16 + int radix = 10; + int value_int = 6; + char value_char = Character.forDigit(value_int , radix); + System.out.println(value_char ); + } +} +``` + +radix 为基数,十进制为 10,十六进制为 16。 + +3)可以使用 int 的包装器类型 Integer 的 toString() 方法+String 的 CharAt() 方法转成 char + +```java +public class SimpleTesting { + public static void main(String[] args) { + 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://tobebetterjavaer.com/basic-grammar/type-cast.html)。 + + + +## 引用数据类型 + +基本数据类型在作为成员变量和静态变量的时候有默认值,引用数据类型也有的。 + +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()` 方法就明白了。 + +![](http://cdn.tobebetterjavaer.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 +/** + * @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 一个: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-07.png) + + +只能 new 一个实现它的类的对象——那自然接口也是引用数据类型了。 + +来看一下基本数据类型和引用数据类型之间最大的差别。 + +基本数据类型: + +- 1、变量名指向具体的数值。 +- 2、基本数据类型存储在栈上。 + +引用数据类型: + +- 1、变量名指向的是存储对象的内存地址,在栈上。 +- 2、内存地址指向的对象存储在堆上。 + +看到这,三妹是不是又要问,“堆是什么,栈又是什么?” + +堆是堆(heap),栈是栈(stack),如果看到“堆栈”的话,请不要怀疑自己,那是翻译的错,堆栈也是栈,反正我很不喜欢“堆栈”这种叫法,容易让新人掉坑里。 + +堆是在程序运行时在内存中申请的空间(可理解为动态的过程);切记,不是在编译时;因此,Java 中的对象就放在这里,这样做的好处就是: + +>当需要一个对象时,只需要通过 new 关键字写一行代码即可,当执行这行代码时,会自动在内存的“堆”区分配空间——这样就很灵活。 + +栈,能够和处理器(CPU,也就是脑子)直接关联,因此访问速度更快。既然访问速度快,要好好利用啊!Java 就把对象的引用放在栈里。为什么呢?因为引用的使用频率高吗? + +不是的,因为 Java 在编译程序时,必须明确的知道存储在栈里的东西的生命周期,否则就没法释放旧的内存来开辟新的内存空间存放引用——空间就那么大,前浪要把后浪拍死在沙滩上啊。 + +这么说就理解了吧? + +“好了,三妹,关于 Java 中的数据类型就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。 + +--- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-grammar/flow-control.md b/docs/basic-grammar/flow-control.md new file mode 100644 index 0000000000..42b50d0b47 --- /dev/null +++ b/docs/basic-grammar/flow-control.md @@ -0,0 +1,922 @@ +--- +title: Java流程控制语句(if/switch判断语句和while/do-while/for循环语句) +shortTitle: Java流程控制语句 +category: + - Java核心 +tag: + - Java语法基础 +description: Java程序员进阶之路,小白的零基础Java教程,Java流程控制语句(if/switch判断语句和while/do-while/for循环语句) +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java流程控制语句,Java判断语句,Java循环语句,if,switch,while,do-while,for +--- + + +“二哥,流程控制语句都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。 + +“比如说 if-else、switch、for、while、do-while、return、break、continue 等等,接下来,我们一个个来了解下。” + +## 01、if-else 相关 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-01.png) + + +**1)if 语句** + +if 语句的格式如下: + +```java +if(布尔表达式){ +// 如果条件为 true,则执行这块代码 +} +``` + +画个流程图表示一下: + +![](http://cdn.tobebetterjavaer.com/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 时执行的代码块 +} +``` + +画个流程图表示一下: + +![](http://cdn.tobebetterjavaer.com/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 时执行的代码 +} +``` + +画个流程图表示一下: + +![](http://cdn.tobebetterjavaer.com/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 时执行的代码 + } +} +``` + +画个流程图表示一下: + +![](http://cdn.tobebetterjavaer.com/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 关键字也是可选的。 + + + +画个流程图: + +![](http://cdn.tobebetterjavaer.com/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 循环 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-07.png) + +**1)普通 for 循环** + +普通的 for 循环可以分为 4 个部分: + +1)初始变量:循环开始执行时的初始条件。 + +2)条件:循环每次执行时要判断的条件,如果为 true,就执行循环体;如果为 false,就跳出循环。当然了,条件是可选的,如果没有条件,则会一直循环。 + +3)循环体:循环每次要执行的代码块,直到条件变为 false。 + +4)自增/自减:初始变量变化的方式。 + + + +来看一下普通 for 循环的格式: + + + +```java +for(初始变量;条件;自增/自减){ +// 循环体 +} +``` + + + +画个流程图: + +![](http://cdn.tobebetterjavaer.com/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(条件){ +//循环体 +} +``` + + + +画个流程图: + +![](http://cdn.tobebetterjavaer.com/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(提交); +``` + + + +画个流程图: + +![](http://cdn.tobebetterjavaer.com/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 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-11.png) + +## 06、break + +break 关键字通常用于中断循环或 switch 语句,它在指定条件下中断程序的当前流程。如果是内部循环,则仅中断内部循环。 + +可以将 break 关键字用于所有类型循环语句中,比如说 for 循环、while 循环,以及 do-while 循环。 + +来画个流程图感受一下: + + +![](http://cdn.tobebetterjavaer.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 +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基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-grammar/javadoc.md b/docs/basic-grammar/javadoc.md new file mode 100644 index 0000000000..e771bca436 --- /dev/null +++ b/docs/basic-grammar/javadoc.md @@ -0,0 +1,191 @@ +--- +title: Java注释:单行、多行和文档注释 +shortTitle: Java注释 +category: + - Java核心 +tag: + - Java语法基础 +description: Java程序员进阶之路,小白的零基础Java教程,Java注释:单行、多行和文档注释 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java 注释,javadoc,注释 +--- + +“二哥,Java 中的注释好像真没什么可讲的,我已经提前预习了,不过是单行注释,多行注释,还有文档注释。”三妹的脸上泛着甜甜的笑容,她竟然提前预习了接下来要学习的知识,有一种“士别三日,当刮目相看”的感觉。 + +“注释的种类确实不多,但还是挺有意思的,且听哥来给你说道说道。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-01.png) + + + + +### 01、单行注释 + +单行注释通常用于解释方法内某单行代码的作用。 + +```java +public void method() { + int age = 18; // age 用于表示年龄 +} +``` + +**但如果写在行尾的话,其实是不符合阿里巴巴的开发规约的**。 + +![](http://cdn.tobebetterjavaer.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」 可以打开命令行窗口。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-03.png) + + +**第二步**,执行 javadoc 命令 `javadoc Demo.java -encoding utf-8`。`-encoding utf-8` 可以保证中文不发生乱码。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-04.png) + +**第三步,**执行 `ls -l` 命令就可以看到生成代码文档时产生的文件,主要是一些可以组成网页的 html、js 和 css 文件。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-05.png) + +**第四步**,执行 `open index.html` 命令可以通过默认的浏览器打开文档注释。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-06.png) + +点击「Demo」,可以查看到该类更具体的注释文档。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-07.png) + +### 04、文档注释的注意事项 + +1)`javadoc` 命令只能为 public 和 protected 修饰的字段、方法和类生成文档。 + +default 和 private 修饰的字段和方法的注释将会被忽略掉。因为我们本来就不希望这些字段和方法暴露给调用者。 + +如果类不是 public 的话,javadoc 会执行失败。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-08.png) + +2)文档注释中可以嵌入一些 HTML 标记,比如说段落标记 `

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

`,因为 javadoc 会插入自己的标题,容易发生冲突。 + +3)文档注释中可以插入一些 `@` 注解,比如说 `@see` 引用其他类,`@version` 版本号,`@param` 参数标识符,`@author` 作者标识符,`@deprecated` 已废弃标识符,等等。 + +### 05、注释规约 + +1)类、字段、方法必须使用文档注释,不能使用单行注释和多行注释。因为注释文档在 IDE 编辑窗口中可以悬浮提示,提高编码效率。 + +比如说,在使用 String 类的时候,鼠标悬停在 String 上时可以得到以下提示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-09.png) + +2)所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。 + +3)所有的类都必须添加创建者和创建日期。 + +Intellij IDEA 中可以在「File and Code Templates」中设置。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-10.png) + +语法如下所示: + +``` +/** +* 微信搜索「沉默王二」,回复 Java +* @author 沉默王二 +* @date ${DATE} +*/ +``` + +设置好后,在新建一个类的时候就可以自动生成了。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/16 + */ +public class Test { +} +``` + +4)所有的枚举类型字段必须要有注释,说明每个数据项的用途。 + +5)代码修改的同时,注释也要进行相应的修改。 + + +“好了,三妹,关于 Java 中的注释就先说这么多吧。”转动了一下僵硬的脖子后,我对三妹说。“记住一点,注释是程序固有的一部分。” + +- 第一、注释要能够准确反映设计思想和代码逻辑; +- 第二、注释要能够描述业务含 义,使别的程序员能够迅速了解到代码背后的信息。 + +完全没有注释的大段代码对于阅读者形同 天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看 的,使其能够快速接替自己的工作。 + +----- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-grammar/operator.md b/docs/basic-grammar/operator.md new file mode 100644 index 0000000000..6797e25d67 --- /dev/null +++ b/docs/basic-grammar/operator.md @@ -0,0 +1,391 @@ +--- +title: Java 运算符 +shortTitle: Java 运算符 +category: + - Java核心 +tag: + - Java语法基础 +description: Java程序员进阶之路,小白的零基础Java教程,本文主要介绍Java 中的运算符(算术运算符、关系运算符、赋值运算符、逻辑运算符、位运算符)的使用 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,运算符 +--- + +“二哥,让我盲猜一下哈,运算符是不是指的就是加减乘除啊?”三妹的脸上泛着甜甜的笑容,我想她一定对提出的问题很有自信。 + +“是的,三妹。运算符在 Java 中占据着重要的位置,对程序的执行有着很大的帮助。除了常见的加减乘除,还有许多其他类型的运算符,来看下面这张思维导图。” + +![](http://cdn.tobebetterjavaer.com/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://tobebetterjavaer.com/basic-grammar/basic-data-type.html)),整型和整型的运算结果就是整型,不会出现浮点型。否则,就会出现浮点型。 + +```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。 + +![](http://cdn.tobebetterjavaer.com/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 年设计。莱布尼茨是德意志哲学家、数学家,历史上少见的通才。 + +![](http://cdn.tobebetterjavaer.com/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参考链接:[http://c.biancheng.net/view/796.html](http://c.biancheng.net/view/796.html),整理:沉默王二 + +--- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/blog.md b/docs/blog.md similarity index 92% rename from docs/src/blog.md rename to docs/blog.md index 2872d26319..dcc247b846 100644 --- a/docs/src/blog.md +++ b/docs/blog.md @@ -1,8 +1,8 @@ --- home: true -layout: BlogHome +layout: Blog icon: home -title: 博客 +title: 博客主页 heroImage: /itwanger.png heroText: 沉默王二的技术博客 heroFullScreen: false @@ -10,7 +10,7 @@ tagline: 技术文通俗易懂,吹水文风趣幽默。 projects: - icon: project name: 进阶之路 - desc: 二哥的Java进阶之路 + desc: Java程序员进阶之路 link: /home.md - icon: link diff --git a/docs/src/cityselect/beijing.md b/docs/cityselect/beijing.md similarity index 90% rename from docs/src/cityselect/beijing.md rename to docs/cityselect/beijing.md index d714a3b019..781de7025b 100644 --- a/docs/src/cityselect/beijing.md +++ b/docs/cityselect/beijing.md @@ -14,7 +14,7 @@ tag: 因为北京适合程序员工作的企业实在太多了,所以怎样给大家介绍北京适合程序员的工作机会一直让我十分头疼。如果写的太简单,我跟大家说北京有百度、腾讯、阿里、美团等等,大家一定觉得我是在废话,但把所有企业一次性全介绍了工作量太大,并且文章也看不出重点了。所以我最终决定,北京拆成三篇来讲,一篇介绍北京的国企央企研究所、一篇介绍北京的大型互联网公司和大型外企、一篇介绍北京的中小型互联网公司。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-68628d58-37ff-44c8-a3f6-6b9f4978f976.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-68628d58-37ff-44c8-a3f6-6b9f4978f976.jpg) 我发现在互联网公司工作的程序员都对国企央企以及研究所十分好奇,但是因为这些单位的信息比较封闭,所以大家对于这些企业的工作方式以及薪酬情况都都不清楚。为了能给北京系列开个好头,第一篇文章我就以我熟悉的北京的国企央企研究所来给大家介绍了。想提前声明一点,虽然我有很多同学和朋友在北京国企央企以及研究所,但是同一个公司,不同的部门或者不同的组差别都太大了。尤其是研究所,每个系列都有数量庞大的下属研究所,并且下属的研究所独立性都特别高,所以风格差别很大。我只能把我见到的情况给大家做一个介绍,具体去到哪个研究所哪个团队还得你们自己去详细打听。只能说我的介绍相对于更接近一些真实。如果大家发现哪里有误,欢迎大家指出。 @@ -30,7 +30,7 @@ tag: #### 工商银行软件开发中心 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-b8653934-cc7f-4703-81f2-f654f7d4440a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-b8653934-cc7f-4703-81f2-f654f7d4440a.png) 和同行相比,工商银行的软件开发中心算技术最好的了。工行喜欢招应届生,社招进来的比较少。刚进来的应届生职级是助理经理,税前年薪差不多是20万左右(到手15-16万吧,加上年终奖和节假日补贴)。工资发放方式稍微有点奇怪,就是每月发八九千,然后留一部分年底一并发放。每月会有饭补,过节也会有节假日补贴(放三天的节日假期给 1000,国庆给 2000,过年 4000)。这个职级的工资对于在北京工作来说还是有点难顶的,毕竟光跟人合租每月就要画掉两千多了。如果你是本科毕业进入工行,从助理经理升到经理一需要两年时间,硕士只需要一年,这个职级基本上全都能按时达到,不过工资变化不太大。从经理一升到经理二大约要一到两年的时间,这就看你能力了,经理二以后工资就相对多一些了,经理二绩效不太好的话每年到手能拿 22 万左右,如果绩效好,可能能拿到 29 万左右。经理二升经理三就完全看你自己能力了,到经理三以后每年到手都能 35 万左右,收入就很可观了。另外在工行发专利什么的也有些奖金,一个专利貌似是奖励 5000. @@ -38,25 +38,25 @@ tag: #### 农业银行软件开发中心 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-fb046afd-1116-4619-a5ae-d9756863ca19.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-58cd1949-b992-4550-b2e1-b24c519c0a78.png) 曾经农行软开的一位领导来我们学校校招宣讲时就说因为银行的工资有限制,涨薪比较慢。所以很多银行软开的职工干几年后就会跑到券商去挣钱。那时我才知道券商居然还招不少程序员。后来我发现中信建投证券是真不错,北京这边校招生进来月薪 18k ,年终奖 6 个月,虽然薪资还是不及互联网公司,但是涨薪速度相比于其它国企央企还是快很多的。另外只有股票开市的日子用上班,加班很少。不过对于学历的要求比较高,必须是985硕士,并且还卡本科。 @@ -64,7 +64,7 @@ tag: ### 运营商 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-1b67f498-dc2e-401f-b942-59455b941bd9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-1b67f498-dc2e-401f-b942-59455b941bd9.png) 这里说的运营商说的是移动联通还有电信的研究院。移动的工资发放风格和工行有异曲同工之妙,每个月发的工资很少,到年终会一起再发一些。应届生年薪大概 20w 左右,平时工资发一半,年终把剩下的一半发给你。联通在北京的研究院有两个,一个叫联通研究院,一个叫联通软件研究院。我听说的是联通研究院比较好,联通软件研究院比较坑。 电信研究院的据说很轻松,但是工资低,没朋友在这里,我不太确定,只是听说。 @@ -80,7 +80,7 @@ tag: ### 航天科技&航空工业 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-fcff3f6d-6357-48fe-a807-74c225ce0c95.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-fcff3f6d-6357-48fe-a807-74c225ce0c95.png) 我有很多的校友都在航空航天系列的研究所。研究所的具体薪资都比较保密,大部分研究所在给你 offer 的时候不会像互联网公司那样把薪资给你说的清清楚楚并且写在合同上,而是大概和你说个数。而且你入职后两到三个月的薪资往往都很少,等三四个月以后薪资稳定了你才能确定你开多少钱。 @@ -90,13 +90,13 @@ tag: ### 工信部下属研究院 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-0f3b7783-4567-4f07-974d-a31b8d9ec939.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-96c5f7a6-b13b-4153-9c7f-7c4617dbddca.png) 中科院系列下计算机相关最强的三个所就是自动化所(搞 nlp 的应该都听过宗成庆老师)、软件所、计算所。另外还有信工所、电子所也还凑合。整体待遇上,中科院系列的待遇不如航空航天系列的。你可以把中科院系列的研究所完全的想象成一个大学(嗯,不用想象,确实是个大学,中国科学院大学嘛)。每个团队就是你们研究生的实验室。基本上就是一个大老板,带几个小老板,再带几个职工,另外再配几个中科院的学生。课题组想要挣钱也是需要大老板出去拉活,然后小老板带着学生去干活。工作方式和技术水平和你们研究生的实验室一模一样的,就是多了一些硕士博士职工罢了。 @@ -104,13 +104,13 @@ tag: ### 中电科系列 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-a4804493-fdb0-48ea-b9f5-82ad9a3d3d08.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-db583262-a5d4-4137-88e1-90f0127ba755.png) 中国兵器系列计算机相关专业招的不是太多,其实每年一共招的人也不多。我最初对中国兵器系列的研究所有了解是一个所来我们学校宣讲。感觉中国兵器在西安的几个所能算的上是西安研究所的天花板了吧,待遇还不错(和航天504所比我有点不太确定)但是在北京我就不太清楚了。 @@ -122,7 +122,7 @@ tag: ## 生活方面 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-799a0d1a-7782-4e3c-8a36-4c495cd94ae5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-799a0d1a-7782-4e3c-8a36-4c495cd94ae5.png) 北京具体的房价、美食教育等情况我放到下一篇讲北京互联网公司的文章中好好去讲下。这篇文章只讲一个事,那就是**北京户口**。北京户口的主要用处就是子女教育问题,其它的一些好处就是有北京户口可以申请共有产权房还有摇车牌等。应届毕业生是最好搞定户口的时候了,如果你能进这篇文章中讲的这些单位,大概率你是有户口的。不过拿户口的同时都要签一个五年的服务期合同,违约金每个单位不一样, 20万-50万不等,逐年递减。对于应届生来说户口貌似吸引力没有特别的大,毕竟现在自己还顾不过来,也不知道自己子女在哪呢。但是对于已经有子女的人来说,我一提给北京户口的事他们眼睛就亮了。 @@ -140,7 +140,7 @@ tag: #### 百度 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-7d1367ae-04c1-4162-aed8-2a99203a623d.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-7d1367ae-04c1-4162-aed8-2a99203a623d.jpg) 坐标:西北旺 @@ -152,7 +152,7 @@ tag: #### 腾讯 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-7d6cd775-8895-447a-a348-27443594bdcd.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-7d6cd775-8895-447a-a348-27443594bdcd.jpg) 坐标:西北旺 @@ -167,7 +167,7 @@ tag: #### 阿里巴巴 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-996bbbca-dd9c-4108-b796-46f29dd840e9.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-996bbbca-dd9c-4108-b796-46f29dd840e9.jpg) 坐标:望京 @@ -177,7 +177,7 @@ tag: #### 字节跳动 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-fd757b6c-154c-4aa6-95a8-467d6a9a83b4.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-fd757b6c-154c-4aa6-95a8-467d6a9a83b4.jpg) 坐标:中航广场 @@ -187,7 +187,7 @@ tag: #### 美团 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-809a1c71-7a8f-43c8-ad4b-befab2fe5cf2.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-809a1c71-7a8f-43c8-ad4b-befab2fe5cf2.jpg) 坐标:望京 @@ -197,7 +197,7 @@ tag: #### 京东 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-e08e1eef-6561-4057-a12e-3a1183fb9ae1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-e08e1eef-6561-4057-a12e-3a1183fb9ae1.png) 坐标:亦庄 @@ -207,7 +207,7 @@ tag: #### 网易 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-fc7b1b08-0363-4af6-adc2-e6185c55bb9c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-fc7b1b08-0363-4af6-adc2-e6185c55bb9c.png) 坐标:西北旺 @@ -223,7 +223,7 @@ tag: #### Hulu -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-483d5154-671c-4ef4-844c-3240f34b6008.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-483d5154-671c-4ef4-844c-3240f34b6008.png) 坐标:望京 @@ -233,7 +233,7 @@ tag: #### 微软 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-e0dbb967-2bb3-453f-8384-02c1c0aa61a0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-e0dbb967-2bb3-453f-8384-02c1c0aa61a0.png) 坐标:离五道口也不远。 @@ -241,7 +241,7 @@ tag: #### Shoppe(虾皮) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-ea99812c-fdb6-44a6-bc55-79c15ad37a3a.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-ea99812c-fdb6-44a6-bc55-79c15ad37a3a.jpg) 坐标:五道口附近,和搜狗挨着。那片有好多互联网公司。 @@ -251,7 +251,7 @@ tag: #### Amazon -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-9a9d93ca-89fa-4b67-a491-d085eb2601d3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-9a9d93ca-89fa-4b67-a491-d085eb2601d3.png) 坐标:朝阳区远洋国际中心 @@ -261,7 +261,7 @@ tag: #### Apple -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-ba5e8a95-1cf8-42f9-aacc-3573cd93f19f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-ba5e8a95-1cf8-42f9-aacc-3573cd93f19f.png) 坐标:朝阳区建国门外大街国贸大厦 @@ -269,7 +269,7 @@ tag: #### Airbnb -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-b9d6ef38-e7c1-4e00-8409-2f63489a5d6c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-b9d6ef38-e7c1-4e00-8409-2f63489a5d6c.png) 坐标:朝阳区环球金融中心 @@ -291,11 +291,11 @@ tag: 下面就说买房的事了,算了,我对北京买房的事也没什么见解。贴个图你们自己看吧,数据不一定准,就感受下房价有多高吧....密云延庆什么的就太远了,当然有很多人在北京上班在天津买房或者廊坊买房的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-e5bc2cf6-647d-43f9-9352-934440d71b16.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/beijing-148db131-6c78-4cbf-86a0-465d88fdd217.png) 有一句话叫“京城无美食”。虽然这句话有点极端但是也有点道理的。美食的聚集地大部分是沿山沿海沿河沿江的地方,北京一直不是一个美食原料富足的地方。北京常说的北京名菜特点不像鲁菜川菜江苏菜那样特点鲜明。我能想到的北京特色的饭也就是烤鸭、涮羊肉、爆肚、炒肝、京酱肉丝、炸酱面,除了这些还真不太好想了。在北京有个好处是你能在这里吃到全国各地的美食,因为北京全国各地甚至世界各地的人都有,所以各省各国风味的餐厅都会有,但是感觉风味上还是根据北京这边的口味做了一些改变。 @@ -303,17 +303,17 @@ tag: ### 教育&医疗 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-dc5e04dd-8a49-45b2-b96d-e1a17a2405f2.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/chengdu.md b/docs/cityselect/chengdu.md similarity index 98% rename from docs/src/cityselect/chengdu.md rename to docs/cityselect/chengdu.md index e1bfef642a..4bad2ccf68 100644 --- a/docs/src/cityselect/chengdu.md +++ b/docs/cityselect/chengdu.md @@ -484,4 +484,4 @@ tag: >链接:[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 +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/cityselect/guangzhou.md b/docs/cityselect/guangzhou.md similarity index 78% rename from docs/src/cityselect/guangzhou.md rename to docs/cityselect/guangzhou.md index bbaedaa35a..650821b4fa 100644 --- a/docs/src/cityselect/guangzhou.md +++ b/docs/cityselect/guangzhou.md @@ -16,7 +16,7 @@ tag: 广州的繁华以及它的魅力不用我多说,无论从人文还是从经济来说都是不虚其它城市的。不过我在面试时发现,广州除了几个头部大厂是统一薪资标准外,相较于其它一线城市,广州的互联网行业给我的感觉是整体薪资水平偏低。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-0f0eb358-f7ae-46b9-b070-8b93e401735e.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-0f0eb358-f7ae-46b9-b070-8b93e401735e.png) 大家注意呀,其实学计算机相关专业的想在广州挣钱,不止可以通过互联网公司,也可以通过当公务员,教师等方式去挣钱呀。应届生考进体制内当公务员,年薪也20多万呢,并且各方面福利待遇绝对到位,相当不错了。下面我们还是分互联网公司以及国企央企研究所介绍吧。 @@ -28,61 +28,61 @@ tag: **网易** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-53893155-f8cb-4bfe-b0b4-768c43c03862.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-6a8653f4-b840-4f46-af16-72fa00b9019b.png) 4399大家应该都不陌生,好多小游戏都是从小开始玩的,工资也在16k左右。 @@ -96,19 +96,19 @@ tag: **移动** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-66cca36c-656b-4e6e-9172-f68d79e78af9.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-ff7fd7ae-3bf8-4797-bf07-f9490da269b9.png) 我看到 offershow 上有做网络运维的爆料,入职第一年每月到手8.5k,年终发了3.5万。 @@ -128,13 +128,13 @@ tag: ### 房价 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-4b8f3cec-ca0d-4b39-9826-755cd2fe29b6.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-75763e2a-3b9b-48e4-a5dc-b0308d60e98f.png) 广州的985大学有两所,分别是中山大学和华南理工,211也有两所是暨南大学以及华南师范大学,另外广州大学、广东工业大学等等也都不错。广州的高中也很给力,华南师范大学附属中学、广东实验中学、广东广雅中学、广州市执信中学、广州二中、六中等等学校都很不错。 @@ -142,17 +142,17 @@ tag: 一线城市的医疗资源肯定是没问题的,我粗略数了一下,广州有不下40所三甲医院。我了解到顶级医疗资源主要有中山系、南方系、广中医系等等。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-5b5c6c29-cc79-4c0b-be57-b105babd0141.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/hangzhou.md b/docs/cityselect/hangzhou.md similarity index 89% rename from docs/src/cityselect/hangzhou.md rename to docs/cityselect/hangzhou.md index 7a0589b3eb..529e869c98 100644 --- a/docs/src/cityselect/hangzhou.md +++ b/docs/cityselect/hangzhou.md @@ -10,11 +10,11 @@ tag: 大家好,我是二哥呀!上期发了南京的互联网公司后,杭州的呼声非常高。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-1.png) -有小伙伴在公众号后台私信催,还有小伙伴在二哥的《二哥的Java进阶之路》的开源专栏提交了 issue,那今天必须得来整一波了。 +有小伙伴在公众号后台私信催,还有小伙伴在二哥的《Java 程序员进阶之路》的开源专栏提交了 issue,那今天必须得来整一波了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-2.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-2.png) 杭州的互联网公司比较多,我先列举一些大家瞅瞅(部分数据来源于好朋友 Carl 的统计)。 @@ -78,7 +78,7 @@ tag: ### 字节跳动 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-3.png) - 「基本情况」 :字节总部在北京,在上海、深圳、杭州、广州、成都等地都有办公室。去年 6 月,抖音电商落户杭州。 - 「业务方向」 :抖音电商、抖音餐饮、字节跳动广告业务、字节跳动本地生活 @@ -89,7 +89,7 @@ tag: ### 阿里系 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-4.png) - 「基本情况」 :阿里系占据了杭州互联网的一片天,相关联的企业是在是太多了。 - 「业务方向」 :达摩院、淘宝、菜鸟、钉钉、飞猪、盒马、支付宝、夸克、UC、书旗小说...... @@ -101,7 +101,7 @@ tag: ### 京东 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-5.png) - 「基本情况」 :京东在杭州也有招聘,不过,招聘的岗位比较少。 - 「业务方向」 :京东金融、京东云 @@ -112,7 +112,7 @@ tag: ### 网易 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-6.png) - 「基本情况」 :网易在杭州有一个研究院,成立于 2006 年 6 月。 - 「业务方向」 :⽹易杭州研究院,简称“杭研”。杭研是⽹易内部的基础技术研发中⼼和前沿技术研究中⼼,在云计算、⼤数据、安全、⼈⼯智能等⽅⾯进⾏前沿技术研究、关键技术攻关和基础技术平台研发,服务⽹易系游戏、邮箱、⾳乐、电商、新闻、在线教育等产品,触达近 10 亿⽤户。 @@ -123,7 +123,7 @@ tag: ### 华为 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-7.png) - 「基本情况」 :华为在杭州有一个研究所。 - 「业务方向」 :智能摄像机、云服务 @@ -135,7 +135,7 @@ tag: ### 中移杭研 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-8.png) - 「基本情况」 :中国移动的一个全资子公司,2014 年在杭州成立。 - 「业务方向」 :统一认证、融合通信、魔固云 @@ -146,7 +146,7 @@ tag: ### 之江实验室 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-9.png) - 「基本情况」 :之江实验室是浙江省委、省政府贯彻落实科技创新思想,深入实施创新驱动发展战略的重大科技创新平台。 - 「业务方向」 :智能感知、智能计算、智能网络、智能系统 @@ -157,7 +157,7 @@ tag: ### 同花顺 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-10.png) - 「基本情况」 :同花顺成立于 2001 年,总部位于杭州未来科技城,是国内第一家互联网金融信息服务业上市公司。同花顺作为一家互联网金融信息提供商,致力于为各类机构提供软件产品和系统维护服务、金融数据服务和智能推广服务,为个人投资者提供金融资讯和投资理财分析工具。 - 「业务方向」 :旗下有多款热门投资理财类 APP。 @@ -168,7 +168,7 @@ tag: ### 51 信用卡 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-11.png) - 「基本情况」 :51 信用卡(母公司为杭州恩牛网络技术有限公司),公司创立于 2012 年,是一家服务于中国亿万信用卡用户的互联网金融公司。 - 「业务方向」 :旗下有“51 信用卡管家”、“51 人品”、“51 人品贷”等 APP @@ -180,7 +180,7 @@ tag: ### 蘑菇街 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-12.png) - 「基本情况」 :2011 年,蘑菇街正式上线,2016 年 1 月与美丽说战略融合,公司旗下包括:蘑菇街、美丽说、uni 等产品与服务。 - 「业务方向」 :电商 @@ -192,7 +192,7 @@ tag: ### 有赞 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-13.png) - 「基本情况」 :有赞,原名口袋通,2012 年 11 月 27 日在杭州贝塔咖啡馆孵化成立,是一家主要从事零售科技 SaaS 服务的企业,帮助商家进行网上开店、社交营销、提高留存复购,拓展全渠道新零售业务。2014 年 11 月 27 日,口袋通正式更名为有赞。2018 年 4 月 18 日,有赞完成在港上市。2019 年 4 月,腾讯领投有赞 10 亿港元融资。 - 「业务方向」 :SaaS 服务(帮助商家网上开店、社交营销......)、PaaS 云服务。 @@ -204,7 +204,7 @@ tag: ### Zoom -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-14.png) - 「基本情况」 :Zoom 是一位美国华人企业家创办的公司,主营业务就是提供视频会议服务。总部位于硅谷,国内的话,杭州、苏州、合肥均有研发中心。 - 「业务方向」 :视频会议 @@ -216,7 +216,7 @@ tag: ### Cisco(思科) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-15.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-15.png) - 「基本情况」 :思科系统公司(Cisco Systems, Inc.),简称思科公司或思科,1984 年 12 月正式成立,是互联网解决方案的领先提供者,其设备和软件产品主要用于连接计算机网络系统,总部位于美国加利福尼亚州圣何塞。 - 「业务方向」 :路由器、交换机等网络基础设施 @@ -234,13 +234,13 @@ tag: 顺带再聊聊杭州的房价、教育吧。杭州的房价不忍直视啊!!!涨的实在是太厉害了! -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-16.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-16.png) 杭州只有一所 985,也就是浙江大学。除了浙江大学之外,浙江省没有 211 院校。不过,杭州电子科技大学虽然不是 211,但是实力很强,外界也很认可。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-17.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/jinan.md b/docs/cityselect/jinan.md similarity index 85% rename from docs/src/cityselect/jinan.md rename to docs/cityselect/jinan.md index 70623d26db..a4a2d3bdff 100644 --- a/docs/src/cityselect/jinan.md +++ b/docs/cityselect/jinan.md @@ -19,21 +19,21 @@ shortTitle: 济南 ### 阿里巴巴 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-52693980-e0eb-4781-94ca-fe15c0363410.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/jinan-f74af12b-73e1-4455-8ad9-1cecda84a21a.png) 对,你没看错,华为在济南是招开发岗的,不过 hc 不多。在济南的华为每年校招的 hc 就是个位数,社招想进去的话基本上就是通过华为 OD 了。 @@ -48,35 +48,35 @@ shortTitle: 济南 ### 海康威视 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-60bc84b2-fd87-4c7a-bcb0-da6134786313.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/jinan-ea15afb0-62d8-4031-9c20-13a04da269d2.png) 薪资差不多是税前 12k x 14 的样子,但是每月工资里面一部分是绩效,如果项目不多的话绩效拿不满。另外出差很多,感觉性价比一般。 @@ -89,7 +89,7 @@ hr 说的税前年薪总包是 15 w,网上的信息比较少,盲猜风格应 相比于济南的工资水平,济南的房价真的是有点高啊。济南的房价前段时间一直跌,但目前在同类型二线城市中,依然算房价偏高的了。我在网上找了个图,不是太准,大家可以做个参考。在济南工资的背景下,在济南年轻人买房压力是挺大呀! -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-6f426bc3-7dbc-4bd8-91c1-4660a945a5f5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/jinan-6f426bc3-7dbc-4bd8-91c1-4660a945a5f5.png) ### 教育 @@ -98,7 +98,7 @@ hr 说的税前年薪总包是 15 w,网上的信息比较少,盲猜风格应 高校的计算机专业资源方面,感觉济南不如青岛,中国海洋大学和中国石油大学都在青岛,山东大学的计算机专业一部分也放在了青岛。这样感觉济南今后计算机人才的积累肯定不如青岛。所以计算机相关行业发展我还是更看好青岛了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-d222ea8c-6d83-4cd6-8cf7-3f7fd2fa1838.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/jinan-d222ea8c-6d83-4cd6-8cf7-3f7fd2fa1838.png) ### 风景 and 交通 and 美食 @@ -107,4 +107,4 @@ hr 说的税前年薪总包是 15 w,网上的信息比较少,盲猜风格应 >作者:大白,转载链接:[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 +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/cityselect/nanjing.md b/docs/cityselect/nanjing.md similarity index 83% rename from docs/src/cityselect/nanjing.md rename to docs/cityselect/nanjing.md index 0e5134dcfc..76626d3ab2 100644 --- a/docs/src/cityselect/nanjing.md +++ b/docs/cityselect/nanjing.md @@ -14,7 +14,7 @@ tag: 有句话说的很对,“安徽不能没有南京,就像东北不能没有三亚”。不过调研中也发现,对于程序员来说,想要在南京留下也不是件很容易的事情,因为南京程序员的工作机会只能算一般,薪资水平这两年许多大公司选择在南京设立分部后才带起来一些,但是南京的房价已经很高了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-707d074e-def7-4cd3-af0a-84e46a0929a9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-707d074e-def7-4cd3-af0a-84e46a0929a9.png) 提一下哈,在这个系列里说的校招薪资都是常规招聘计划的薪资,说的常规招聘计划给出的 offer 包含普通 offer,sp (special offer) 和 ssp。不包含那些比如华为天才少年、美团北斗之类的。那种神仙 offer 怎么样我这种凡人还真不了解。这些招聘的薪资也都是针对本科生和研究生的。如果你是博士,薪资方面和公司都好商量。 @@ -24,7 +24,7 @@ tag: ### 阿里巴巴 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f55e084c-f731-419b-af71-278ac8e7b15c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-f55e084c-f731-419b-af71-278ac8e7b15c.png) 校招薪资:招的人不多,具体薪资情况不太清楚。据说和总部薪资水平差不多。阿里的薪资水平可以参考下[链接](https://mp.weixin.qq.com/s/3c-a7GpkTzAU2DFWSwN4lw)。 @@ -32,7 +32,7 @@ tag: ### 字节跳动 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f945934a-3ab8-4272-a8b2-7001ea396377.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-f945934a-3ab8-4272-a8b2-7001ea396377.png) 校招薪资:字节在南京的薪资是字节在北京上海深圳薪资的 0.9。感觉薪资水平挺不错了。今年字节校招薪资也可以点这个[链接](https://mp.weixin.qq.com/s/3c-a7GpkTzAU2DFWSwN4lw)查看。 @@ -40,7 +40,7 @@ tag: ### 荣耀 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-8470d707-53c6-4c07-8160-50759ad9d50b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-8470d707-53c6-4c07-8160-50759ad9d50b.png) 校招薪资:月薪 17k - 27k(对应13-15级),每年 14-16 薪。 @@ -50,7 +50,7 @@ tag: ### 华为 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f8ba1f60-c3bc-450b-b833-54db9bfdb807.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-f8ba1f60-c3bc-450b-b833-54db9bfdb807.png) 校招薪资:13级月薪 20-21k,14 级 23-25k,15级月薪 26-27k。每年 14-16 薪。16级我就不清楚了。 @@ -58,7 +58,7 @@ tag: ### 小米 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-bfb9cd80-f9b5-4ea7-b4f6-a15e54cf3c4b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-bfb9cd80-f9b5-4ea7-b4f6-a15e54cf3c4b.png) 校招薪资:小米的薪资是挺抠,不过放在南京来看也还行。看校招薪水上南京的薪资爆料今年有 11k,14k,16k,18k,19k,21k,23k 几个档。11k,14k基本都是本科生,硕士做开发岗的大部分是16k,18k,19k这几个档。20k 以上的基本都是做算法的。 @@ -66,7 +66,7 @@ tag: ### Vivo -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-7d676f6f-b081-4eb7-a2ce-3590a04b2fcd.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-7d676f6f-b081-4eb7-a2ce-3590a04b2fcd.png) 校招薪资:南京这边目前看到给应届生的价钱分为月薪17k,20k,24k三个档,,每年15薪。额外每个月有 1500 的租房补贴。住房公积金5%。 @@ -74,7 +74,7 @@ tag: ### OPPO -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-9ad6c7fe-c486-4628-8e07-f21b3e2cac6d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-9ad6c7fe-c486-4628-8e07-f21b3e2cac6d.png) 校招薪资:具体薪资不太清楚,大致应届生月薪在 20k 上下,每年 14 -15 个月薪资。 @@ -82,7 +82,7 @@ tag: ### 中兴 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-094809df-30b0-40c6-a1a5-f1e0a5329b6a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-094809df-30b0-40c6-a1a5-f1e0a5329b6a.png) 校招薪资:中兴常规计划(指的是普通 offer)的月薪基本都在20k 以内,蓝剑计划年薪会在 40w 以上。 @@ -90,7 +90,7 @@ tag: ### 360 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-0bec3704-902d-4b8b-90b7-4ab5fc28a79d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-0bec3704-902d-4b8b-90b7-4ab5fc28a79d.png) 校招薪资:没调查清楚,年薪差不多 23w 左右。 @@ -98,7 +98,7 @@ tag: ### 深信服 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-5041109f-e9d8-4419-9f41-466f93708c53.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-5041109f-e9d8-4419-9f41-466f93708c53.png) 校招薪资:开发岗有3个档,月薪分布在15.6 - 22.8k 这个区间,算法岗也是三个档,月薪分布在19 - 25.2k 这个区间。每年13 - 15 薪 @@ -106,7 +106,7 @@ tag: ### 趋势科技 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f60ae6ee-13d7-4efc-bd5a-a0334b76c6e3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-f60ae6ee-13d7-4efc-bd5a-a0334b76c6e3.png) 校招薪资:看到校招薪资爆料有三个档,分别是年薪 20w,23.5w 和 25w。 @@ -114,7 +114,7 @@ tag: ### 亚信安全 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-97e8edd1-2a4d-492c-9039-febbae881c05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-97e8edd1-2a4d-492c-9039-febbae881c05.png) 校招薪资:基本上月薪在15-18.5k 这个区间,每年 13- 15薪。 @@ -122,7 +122,7 @@ tag: ### SHEIN -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-fc7ddb5b-cbc1-482f-9087-ea9e3e136795.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-fc7ddb5b-cbc1-482f-9087-ea9e3e136795.png) 校招薪资:目前看校招薪资爆料月薪基本集中在16-23k,有个说自己 27k 的不知道真的假的(如果是真的,感觉 SHEIN 大部分老员工要RUN了)。每年14薪。 @@ -130,7 +130,7 @@ tag: ### 满帮集团 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-ec98654b-0bcf-4e96-bb52-fe7ebd2a6c55.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-ec98654b-0bcf-4e96-bb52-fe7ebd2a6c55.png) 校招薪资:校招薪水上的爆料开发月薪基本都在 20-30k 这个区间,算法基本都在 27-36k 这个区间。大部分人每年 14 薪。 @@ -140,25 +140,25 @@ tag: ### 中电系列 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-21fd091d-8afd-4cde-8fbd-a78b3c805085.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-d16dd7fe-f7a0-4f70-a016-f88350a56fa7.png) 简单说下烽火,下面有烽火星空还有烽火通信。是国企,不过感觉一直处于一个乙方的位置。项目很多是涉密项目,但是我劝大家尽量少碰涉密类型的项目。如果手头没大公司可以去烽火锻炼下,如果有其它大公司的 offer,我感觉还是优先其它大公司了。 @@ -170,18 +170,18 @@ tag: 南京的房价还是比较高的,比较好的区域的房价直逼一线城市了,对于刚毕业的学生来说压力很大了,感觉这个房价应该逼走不少人。 另外南京物价也不低噢。下面这张图是网上找的,不一定准,可以当作参考。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-b5247968-f8fa-445a-b83e-7d88fbb056c7.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-9da3efca-ceb7-4685-9932-6613ea2ea1c6.png) 南京拥堵程度是上过全国前十榜单的,我租房是喜欢宁愿住的小一点差一点也尽可能离上班地方近一点,我比较抗拒通勤。宏观上来说,南京的地理位置特别好,离苏州、杭州、上海都很近,另外离安徽也很近,这也是安徽人喜欢往南京跑的原因。 @@ -194,4 +194,4 @@ tag: >作者:大白,转载链接:[https://mp.weixin.qq.com/s/CfZ1CEmtPOP4TAwAs8Ocrw](https://mp.weixin.qq.com/s/CfZ1CEmtPOP4TAwAs8Ocrw) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/qingdao.md b/docs/cityselect/qingdao.md similarity index 78% rename from docs/src/cityselect/qingdao.md rename to docs/cityselect/qingdao.md index f100d03eda..13033aac4f 100644 --- a/docs/src/cityselect/qingdao.md +++ b/docs/cityselect/qingdao.md @@ -14,7 +14,7 @@ tag: 当时我妹还在沙滩上留了一张“沉默王二在青岛”的印迹。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-938dc8a2-9b47-4e12-aa45-79c2caf94f04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-938dc8a2-9b47-4e12-aa45-79c2caf94f04.png) 按照我妹的说法,“青岛真是一座网红城市,美女是真多!” @@ -26,13 +26,13 @@ tag: ### 海尔 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-e656449e-a34f-4915-a1cb-459208d23e34.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-9645cf8d-d9dc-4de7-a192-bdb9a447ec49.png) 我之前总觉得海信和海尔有某种联系,不过我查了下并没有,路过海信的园区时觉得海信也修的挺漂亮的。一个 211 硕士爆料的薪资是每月 10k,然后一年 13 个月工资。福利待遇方面感觉还可以,前两年有免费宿舍,交通补贴每月 200,住房补贴 800 每月,可以领三年。试用期 6 个月,试用期间 90% 薪资,免费班车,公积金 10%,社保全额。工作早9晚6,中午休息一小时。 @@ -40,43 +40,43 @@ tag: ### 光大银行青岛研发中心 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-7351e8f8-90d5-4f3c-998a-703b36ec1fca.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-53f614bb-d10d-4ae2-9f83-5911ef8ddead.png) 说实话,我一直对中电系列的研究所印象不是很好,不过听说中电28所的薪资后对中电系列的印象有所改观(这里是闲扯一句哈,中电28所在南京,和青岛没啥关系)。注意41所得总部是在安徽,青岛只是一个分部,我了解到是加班会比较严重。 @@ -88,7 +88,7 @@ Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330 青岛房均价两万多了吧。崂山和市南比较贵,其它地相对好一些。下面这张图有各区的房价,不是太准,不过可以参考下呀。感觉程序员在北京干几年,攒个首付,然后回青岛找个相对稳定的公司上班,买房压力还不是很大(除了买崂山和市南啊)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-a228e29e-363f-44a3-b8cc-6cdd18d34b9f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-a228e29e-363f-44a3-b8cc-6cdd18d34b9f.png) ### 教育 @@ -96,7 +96,7 @@ Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330 大学方面,山东大学在青岛是有校区的,虽然离市区是真的远,但是人家有地铁呀。我记得坐那趟地铁去山东大学青岛校区找我舍友时穿越崂山,窗外的风景是真的漂亮。中国海洋大学也是一所985,校区是真的漂亮。中国石油大学是一所211,也挺不错的。另外像青岛大学、青岛理工大学、山东科技大学这些院校也是不错的。这几年各个高校也在青岛修建了研究院,感觉还是挺有发展前景的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-b37788d7-c33d-475a-bc69-05927bf0825d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-b37788d7-c33d-475a-bc69-05927bf0825d.png) 高中方面,青岛二中、青岛一中、青岛五十八中、青岛九中、即墨一中等都非常好。不过山东高考的压力是真的大,感觉我大学和读研时身边的山东同学,当年高考都是英雄般的人物。 @@ -104,11 +104,11 @@ Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330 青岛是真的挺美的,感觉在青岛既能感觉到现代化大都市的感觉、又能体验到民国风情还能体验到欧式风情。另外我同学带我在青岛转时,经常走着走着就见到了大海,这让我这个多年身居内陆,没见过海的少年来说还是挺欣喜的。感觉青岛比西安冷很多,今年4月西安已经很热了,花都开了,但是去了青岛以后发现柳树才刚有一点点绿,还挺凉快的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-d619e60b-732b-497f-a4a0-ebbad7ce1ce9.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-ce0b22f6-0b52-425c-a5d0-4eefe52706f6.png) ### 交通 @@ -117,4 +117,4 @@ Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330 >作者:大白,转载链接:[https://mp.weixin.qq.com/s/8QQvOrkG3Vdjj3GxP1zxBQ](https://mp.weixin.qq.com/s/8QQvOrkG3Vdjj3GxP1zxBQ) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/shenzhen.md b/docs/cityselect/shenzhen.md similarity index 97% rename from docs/src/cityselect/shenzhen.md rename to docs/cityselect/shenzhen.md index ec27cf797a..d313f688d7 100644 --- a/docs/src/cityselect/shenzhen.md +++ b/docs/cityselect/shenzhen.md @@ -83,5 +83,5 @@ AI独角兽公司 >作者:代码随想录,转载链接:[https://mp.weixin.qq.com/s/hBU-eEUq8aN5PWwdZFmC4g](https://mp.weixin.qq.com/s/hBU-eEUq8aN5PWwdZFmC4g) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/suzhou.md b/docs/cityselect/suzhou.md similarity index 83% rename from docs/src/cityselect/suzhou.md rename to docs/cityselect/suzhou.md index cbaa3f299e..b98ea0bfcc 100644 --- a/docs/src/cityselect/suzhou.md +++ b/docs/cityselect/suzhou.md @@ -22,7 +22,7 @@ tag: 有句话叫做上有天堂,下有苏杭,不过有一说一,互联网环境方面苏州和杭州感觉还是有点差距。但是个人觉得还是不错的,而且从 18 年左右开始,苏州的互联网环境开始有了明显的改善。生活方面无论是苏州的地理位置,还是人文环境,都是十分适合居住生活的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-e780c8c6-6e3f-4ba0-a91b-5cf572ee2d54.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-e780c8c6-6e3f-4ba0-a91b-5cf572ee2d54.png) 老规矩,下面我们还是按照程序员的工作机会和生活环境来介绍苏州。 @@ -32,7 +32,7 @@ tag: ### 微软 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-e18a11b0-2667-4e27-b93f-a05dad122e76.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-e18a11b0-2667-4e27-b93f-a05dad122e76.png) 微软可以说是程序员在苏州最好的工作机会了。微软近些年也一直扩大在苏州的投资力度,目前微软苏州研究院的研发团队已经有 2000 人左右了,并且还在近一步扩建。2022 年微软将启动微软苏州三期新大楼的建设,建成后预计微软在苏州的研发团队会达到5000人。所以,目前想拿微软苏州的 offer 的难度,相比于拿北京上海等其它几个工作地 offer 的难度会低一些,大家抓紧呀。 @@ -44,55 +44,55 @@ tag: ### ZOOM -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-ce9a78ef-6a18-4adf-8a4f-de7b7fa34b45.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-76122245-a8e1-44a8-a1f4-8ce7ab3c640d.png) 上面是我认为苏州的比较不错的程序员就业机会,其它的一些苏州比较成规模的提供程序员就业机会的公司就列在下面,并且快速的简单介绍一下。 @@ -120,7 +120,7 @@ tag: ### 教育 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-1b300d4a-1384-4dd2-a9a5-98221f1f6c46.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-1b300d4a-1384-4dd2-a9a5-98221f1f6c46.png) 高等教育方面。其实就苏州本身而言,高校并不多,只有苏州大学一所 211(苏大的自然语言处理挺强的)。但是前些年许多地理位置偏北的高校为了打造自己在南方的影响力,提高生源质量,都会选择在南方办学。其中一部分选择了深圳,另一部分选择了苏州。西交大、中科大、中国人民大学、东南大学、西工大等院校都在苏州设有校区。所以虽然苏州本地院校不多,但是苏州计算机软件相关的高校毕业生并不少。不过这种现象随着西工大太仓校区刚修好,异地办学就被叫停了,我瓜实惨~ @@ -130,11 +130,11 @@ tag: 相比于苏州的工资水平,其实苏州的房价也不低了,看网站上的新房均价大约两万五一平。但是和旁边的城市一比,那苏州的房价就比较香了。在苏州咬咬牙还是能考虑买房的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-60675498-4bfe-46d2-af70-da2f21add403.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-c35c16ca-0bbf-489a-ba0b-9ecb6fef6905.png) 娱乐方面苏州好玩的地方很多,传统的苏州园林风格和苏州现代风格都让人十分留恋。周末在苏州园林逛一逛真的很惬意。 @@ -147,4 +147,4 @@ tag: >作者:大白,转载链接:[https://mp.weixin.qq.com/s/cnYsZLudFOwv5EKYMsMh0Q](https://mp.weixin.qq.com/s/cnYsZLudFOwv5EKYMsMh0Q) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/xian.md b/docs/cityselect/xian.md similarity index 87% rename from docs/src/cityselect/xian.md rename to docs/cityselect/xian.md index 23ef78967a..2817fb0d50 100644 --- a/docs/src/cityselect/xian.md +++ b/docs/cityselect/xian.md @@ -31,7 +31,7 @@ tag: **华为** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-4e82d879-bbe0-4bc5-a088-fadc84483d65.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-4e82d879-bbe0-4bc5-a088-fadc84483d65.png) 如果想在西安快速挣钱的话,华为几乎是最好的选择了,按照华为的工资水平在西安买房根本没有压力。华为在西安的建制很齐全,消费者、CloudBu、云核心等事业群以及`华为海思`,2012 实验室都有。但是因为去年美国对华为的打压,消费者、华为海思以及 2012 实验室这些之前很香的事业群目前日子都不太好过。 @@ -45,13 +45,13 @@ tag: **阿里巴巴** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-135b1f20-48ad-4295-bc50-40969ef023b0.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-c9b0621d-f56c-42a0-a618-f261466f06a4.png) 京东把京东物流的团队设在了西安航天城,目测团队规模在几百人,工资水平大约是京东在北京工资的 80%。大家可以参考一下,京东去年在北京的部门,校招普通 offer 是年包 28 万,sp 是年包 32.9 万。 @@ -59,7 +59,7 @@ tag: **腾讯云** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-67aa194e-52b0-4acb-b22b-1c1f4c09fac8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-67aa194e-52b0-4acb-b22b-1c1f4c09fac8.png) 腾讯云是腾讯的全资子公司,目前对这里褒贬不一。校招薪资水平本科和硕士生大概是月薪 13-16k,每年 16 薪,大家可以参考一下,腾讯 21 届校招的薪资是 17-21.5k ,每年 18 薪。社招我看一个本科毕业四年经验的老哥拿到的年包是 33 万。腾讯云的职级和腾讯不是对齐的,并且业务比较边缘,这也是网上被喷的一个主要原因。目前西安腾讯云创立时间不久,加班强度是比较大的。 @@ -69,7 +69,7 @@ tag: **广联达** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-9180be40-265e-42e5-9a85-4d61193b8798.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-9180be40-265e-42e5-9a85-4d61193b8798.png) 广联达西安的部门在针对 21 届毕业生的校招中开出的薪资是很有诚意的。给应届硕士的 sp 是 19k x 15,普通 offer 是 17k x 15,本科生每月基本都在 13-17k 之间。广联达工作制度基本是 965 或者 975,每个月还有一天的带薪病假。这还要啥自行车?不过广联达曾经也有过黑料,大家自行上网了解。 @@ -79,7 +79,7 @@ tag: **360** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-327db5f0-4061-48c3-b672-b1670f98325f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-327db5f0-4061-48c3-b672-b1670f98325f.png) 360 在西安只有一个几十人的团队。没听说过这里有校招,社招的话 3 年以上经验的差不多能每月给到 20k 。有说在这里待的舒服的,也有喷的。 @@ -89,7 +89,7 @@ tag: **科大讯飞** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-3592f841-8390-4399-ae8a-94c4368541ea.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-3592f841-8390-4399-ae8a-94c4368541ea.png) 科大讯飞西安丝路总部主要是算法岗,不过我看网上喷的比较多。据说是活多钱少。 @@ -103,7 +103,7 @@ tag: **绿盟** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-b70c6624-f4f1-453f-9ec8-5db22661bb18.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-b70c6624-f4f1-453f-9ec8-5db22661bb18.png) 绿盟算是中型企业里比较香的,绿盟在安全领域还是比较强的。工资比较低,硕士校招才能给到 14k x 14,但是这家公司几乎不加班,员工的离职率也一直很低,我同学有违约中兴三方去绿盟的。 @@ -119,7 +119,7 @@ tag: **寒武纪** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-fdf08fc3-d216-4382-913d-5f8b78b8e7df.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-fdf08fc3-d216-4382-913d-5f8b78b8e7df.png) 寒武纪是做智能芯片的公司,背后站着中科院计算所。2019 年落户的西咸新区,相关信息比较少。我知道的是加班比较多,不过薪资也高。 @@ -133,7 +133,7 @@ tag: 感觉西安几家国企的性价比略低,工资不高,且大部分加班严重。校招应届硕士工资税前年薪基本都在 15w-20w 之间,本科生的年薪比硕士少 3w 左右。国企相较于私企涨薪会慢很多,不过相对稳定一些。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-e12acc1d-4b6d-4f2e-a352-7155b527fa07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-e12acc1d-4b6d-4f2e-a352-7155b527fa07.png) **中兴** @@ -145,7 +145,7 @@ tag: **联通** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-33ac9a8b-04d3-4adb-8482-e725e25bb2f9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-33ac9a8b-04d3-4adb-8482-e725e25bb2f9.png) 西安联通软件研究院,应届硕士的总包和移动差不多,大约 16-19 万。不过联通比较清闲,大部分部门都能下午六点就下班。目前在网上的评价相对好一些。 @@ -163,7 +163,7 @@ tag: **荣耀** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-19d21a8f-6a45-408a-9f0f-ae8dd40bdbb1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-19d21a8f-6a45-408a-9f0f-ae8dd40bdbb1.png) 荣耀目前的招人需求是很大的,工作强度未知,薪资水平目前是完全对标华为的。 @@ -181,7 +181,7 @@ tag: **农行软开** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-cfa06bd9-abd6-455e-bd5c-aef4c90f7edb.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-cfa06bd9-abd6-455e-bd5c-aef4c90f7edb.png) 农行软开目前是在西安工作的最好的几个选择之一,硕士每月到手 12k,本科生少几百块钱,年终奖 2-4 个月。大部分部门都能晚上 7 点以前下班,并且周末双休。目前农行软开有子公司化软件开发中心的计划,听消息说可以选择去子公司,也可以留在软开。目前西安的农行软开也越来越卷,大部分的 offer 都给了西电、西交、西工大这三个学校了,另外这三个学校的学生现在也不是想去农行就能去了。农行软开有个硬性规定是必须通过六级才能报。 @@ -207,7 +207,7 @@ tag: **邮政银行** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-946ce62c-0afc-48c8-b570-fab660d2ea13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-946ce62c-0afc-48c8-b570-fab660d2ea13.png) 邮储银行软件开发中心在西安刚成立,还不太确定。目前宣传是年包 28 万以上,工作强度目测比较大。 @@ -221,7 +221,7 @@ tag: **三星** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-31302c7c-7dcd-4f12-b624-872b4878670d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-31302c7c-7dcd-4f12-b624-872b4878670d.png) 大部分应届生都是(11-14)k \*13.5k。965 工作制。几乎都是芯片、运维相关岗位。 @@ -237,7 +237,7 @@ SAP 是一家做企业软件的德企,技术十分强大。硕士年薪 20 万 **Thougtworks** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-167d941b-740e-4342-9d28-1ba753766398.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-167d941b-740e-4342-9d28-1ba753766398.png) 在 Thoughtworks 工作是很舒服的,开放式办公、扁平化管理、技术氛围浓厚。工资本硕都是 13k x 14。 @@ -253,7 +253,7 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW ### 研究所 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-872dc3ff-316d-4194-b549-f998b39feedf.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-872dc3ff-316d-4194-b549-f998b39feedf.png) 西安航空航天类的研究所特别多,我知道的招计算机方面的研究所有航天 504 所、771 所,航空 631 所、618 所、603 所,中电 20 所、39 所,兵器工业 203 所、204 所、205 所。 @@ -265,7 +265,7 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW ## 生活 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-980c455e-1858-4c52-8c4f-5c1b0ed996b4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-980c455e-1858-4c52-8c4f-5c1b0ed996b4.png) 陕西的资源基本都集中在西安,从人口上也能看出西安的资源有多集中。整个陕西三千多万人,在西安就有一千多万。并且这些年中央对西安的扶植力度越来越大。 @@ -273,23 +273,25 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW 西安的房价从 18 年到现在翻了一倍,但就目前的房价相较于其它同类型城市算是比较友好的。现在西安的房价最贵的在曲江、第二贵是高新区。其它地方的房价差不多一万六左右吧,不过今年的全运会过后可能会长一波。按照程序员的工资来说,在西安买房的问题不算很大,这也是程序员待在西安比较舒服的地方。对于程序员来说,租房的压力相对较小,我同学有在农行软开工作的,在附近租了一个一居室的开间四十平左右,一个月一千五,上班步行用不了十分钟。高新那边租房贵一些,你愿意合租的话压力也不大。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-b905fada-e4a5-4a0a-a704-cbf9f310be6c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-b905fada-e4a5-4a0a-a704-cbf9f310be6c.png) 西安住建前段时间出了二手房交易参考价格,我贴在下面大家可以参考下,不过这个价格感觉低于市场价了。我感觉这直接打了个八折~西安住建发布二手房交易参考价格的链接在这里 https://mp.weixin.qq.com/s/Gis7kIJklWygTseztydDaw +![image-20210720212848036](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-c22be12f-8426-487d-b256-96d7e8950762.png) +![image-20210720212821035](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-fb06e9b2-ba0b-46d3-b84a-5449a77dc6ae.png) ### 教育资源 西安的教育资源很好。高中教育资源方面,西安的名校众多。西工大附中、西安铁一中、高新一中、交大附中、陕师大附中这些学校在全国都是很有名的,另外还有一批在陕西省很有名的高中也很不错。大学教育资源方面,整个陕西有三所 985,西工大和西安交大都在西安,西北农林科技大学就在离西安不远的杨凌。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-d905fc96-b354-45fa-b6aa-cfb4e2171eb7.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-e6f524ec-e353-48c2-b89a-cd2ca6373924.png) 医疗资源方面西安也很给力,交大一附院、交大二附院、唐都医院、西京医院、红会医院都是放在全国都很强的,另外其他一批省内比较有名的医院也很不错。 @@ -297,7 +299,7 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW 西安的交通方面不敢恭维,堵车那是一绝,我的感觉是西安比北京都要堵。西安的地铁 21 年初新开了 3 条线路,目前共有 8 条线路才勉强够的上需求。每逢法定节假日,旅游的人都会把西安挤炸。说到旅游,近年来西安对游客的吸引力是越来越大,一方面西安在弘扬大唐文化,另一方面西安的美食也叫一个美滴很。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-e3de7963-bfcf-4b37-9306-ff9939202208.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-e3de7963-bfcf-4b37-9306-ff9939202208.png) 另外,大唐不夜城这里的人是真的多,尤其是夏天,晚上的时候基本打不到车! @@ -311,8 +313,7 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW 综上所述,西安目前正处在高速上升的阶段,互联网行业相对北上广深杭还有一定的差距,相比与成都也还稍差一点。但是西安绝对是有潜力的,并且目前西安的房价还是比较友好的。大家如果能选择在西安发展,生活幸福感会比较高。 -更多西安的信息,可以戳这个链接:[西安互联网](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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/cityselect/zhengzhou.md b/docs/cityselect/zhengzhou.md new file mode 100644 index 0000000000..b20c165961 --- /dev/null +++ b/docs/cityselect/zhengzhou.md @@ -0,0 +1,279 @@ +--- +shortTitle: 郑州 +category: + - 求职面试 +tag: + - 城市选择 +--- + +# 郑州都有哪些值得加入的IT互联网公司? + + +首先我们来看工作机会! + +一位读者的评论我觉得特别的好,我贴到这里给大家看下: + +网名"咔嚓":作为在郑州工作的前端,表示遇见好厂的话,生活节奏还是很爽的,房租不贵,直接住公司旁边,通勤5分钟,不加班的话,每天晚上6点下班,双休。确实郑州互联网不强,但是,我们也不应该忘了生活本该有的样子啊。 + +## 工作机会 + +郑州的互联网资源还是比较匮乏的,究其原因,我觉得和教育资源的匮乏有非常大的关系。 + +教育资源极度匮乏导致好的企业不来,好的企业不来又导致人才外流,恶性循环。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-0cd5c4ac-4e67-4b06-b5a3-20fad836824b.png) + + + +### 数字郑州 + +![img](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-b88e7ee0-5ab7-40af-8d1d-16cd3eae7b97.png) + +这个是阿里和郑州的政府合作的,目前评价大家对数字郑州的评价很不错呀,薪资也挺给力的,大家可以看下 Boss 上数字郑州的招聘岗位以及薪资报价呀。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-b1ef6dc9-0859-47fd-9058-a504bf01cd5a.png) + +### 中原银行 + +![img](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-db7b6919-2158-4846-914f-f30e028c0234.png) + +中原银行的工资比较高,在郑州生活的话去中原银行是很不错的选择,不过想进中原银行的话,不是校招想进去有点难。薪资水平可以看下 Boss 上的招聘薪资水平。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-ce9e2a6e-8743-49b3-83aa-8719da78b7e1.png) + + + +### 浪潮 + +浪潮在郑州的研发中心法定节假日加班是有加班费的,但平时加班就没有加班费了,每月要求加够50个小时的班。薪资水平大家也可以参考下 Boss 上放出的招聘薪资水平。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-ef0fe9a0-eff2-4037-96d2-ab84a2f76491.png) + +### 新华三 + +新华三大部分情况下能双休,周末加班也有加班费,不过涨薪很缓慢。在网上看到一个帖子,有人问 offer 选西安中兴还算郑州新华三,中兴和华三的职工都在互相劝退,说这是一个送命题。薪资水平大家还是参考下Boss上的招聘薪资水平吧。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-caa46657-f65d-4965-b553-c9f4502f3cc9.png) + +![点击并拖拽以移动](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-62496f27-fe20-45c6-b116-90a38e607a09.jpg) + +### UU 跑腿 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-624dd61d-2b19-4cb6-b4b6-e1b813fd6baf.png) + +UU 跑腿主要提供同城送件服务,是郑州本土最大的互联网公司,隶属于郑州时空隧道信息技术有限公司,地址位于郑州市金水区。 + +UU 跑腿的工作环境以及各种福利都还算不错! + +面试的话,总体体验还不错,技术面试一般问的还比较全面。拿 Java 后端开发来说,像 SQL 优化、分布式、缓存这些一般都会问到。 + +薪资的话,看准网上的平均薪资是 10k 附近,其中后端开发的薪资在 14k 附近,前端开发的薪资在 10k 附近,软件测试的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-b3a5d176-a10b-4637-a349-06197ee984a8.png) + +### 中原消费金融 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-3317ba70-8310-465c-b36d-60f8e215960c.png) + +河南中原消费金融股份有限公司是一家全国性非银行金融机构,地址位于郑州市郑东新区。 + +中原消费金融的办公环境非常不错,薪资福利相对也还不错。 + +整体面试体验不错,效率也非常高,像技术面试的话一般是三轮或者四轮。不过,中原消费金融比较看重学历,985/211 上岸的几率比较大。 + +薪资的话,看准网上的平均薪资是 16k 附近,其中后端开发的薪资在 17k 附近,前端开发的薪资在 16k 附近,软件测试的薪资在 14k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入,应该到不了这么高)。 + +注意:大家注意一个情况,中原消费的软件研发岗位大部分都搬迁到上海了,目前在郑州的大部分是行政岗位,只有少部分研发岗位。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-6de95df2-4c10-44af-9ddb-935cb358def6.png) + +### 刀锋互娱 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-aa02294b-e809-49b6-9e96-4ec44db676c3.png) + +刀锋互娱是一家专注游戏服务市场的互联网公司,2019 年完成 A+轮融资,平台注册用户量突破千万。 + +旗下比较出名的产品有租号玩、一派陪玩,都是和游戏领域相关的产品。相信比较喜欢玩游戏的小伙伴应该对这个两个产品有了解。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-ec100440-a50e-4f27-88e4-4b075cf12676.png) + +整体面试不是很难,薪资相对来说也还可以。 + +薪资的话,看准网上的平均薪资是 16k 附近,其中后端开发(C++)的薪资在 20k 附近,前端开发的薪资在 8.5k 附近,软件测试的薪资在 9.5k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-1b7640e6-deee-42f6-8df7-639555c5d236.png) + +### 新开普 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-ca1cf626-13aa-4081-b6c0-285893458dde.png) + +新开普也是郑州的一家本土互联网公司,成立于郑州高新技术产业开发区,主要做 NFC 近场移动支付、金融 IC 卡等业务。 + +新开普是目前国内一卡通行业唯一一家上市公司,已经为全国千所高校,千万名大学生提供服务。 + +技术面试的话,一般第一面是笔试,笔试之后会再问你一些相关的技术问题。 + +薪资的话,看准网上的平均薪资是 7.6k 附近,其中后端开发(Java)的薪资在 9k 附近,前端开发的薪资在 9k 附近,软件测试的薪资在 5.5k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-dca4c23e-7310-4a60-b338-25f3e92d3339.png) + +### 中移在线 + +中国移动旗下的一家“互联网”公司,实际不像是“互联网”公司。 + +对于技术开发来说,去中移在线一是对技术没有提升或者挑战,二是工资是真的低(看准网上的 Java 开发薪资在 8k 附近)。 + +真心不太建议去,除非你没有其他更好的选择。 + +我能想到唯一的优势可能是公司相对来说能提供给你的一个相对稳定的工作。 + +### 爱云校 + +爱云校常见于 2014 年,主要做的是在校教育这块,致力于通过 AI 建一所云上的学校。 + +单看公司所做的业务方向来说,发展相对来说还是不错的。不过,据说公司的管理真的是渣到了一定程度。 + +另外,根据大部分面试求职者的反馈来看,这家公司的整体面试体验比较差。 + +薪资的话,看准网上的平均薪资是 12k 附近,其中后端开发(Java)的薪资在 14k 附近,前端开发的薪资在 8k 附近,软件测试的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-90273777-d5d0-424a-a8b0-e5df53455ef9.png) + +### 妙优车 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-b6ca12ab-ecb5-4b01-9cf3-21b22da0d67a.png) + +妙优车主要做的是汽车方面的业务,涵盖整车销售、汽车金融、汽车保险、汽车用品、汽车美容等方面。 + +公司发展一般,网上也有一些黑历史(可以自己查一下)。 + +不过,根据大部分面试求职者的反馈来看,这家公司的整体面试体验还是可以的。 + +薪资这块的一般偏上,看准网上的平均薪资是 11k 附近,其中后端开发(Java)的薪资在 12k 附近,前端开发的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-491c2bc1-5cea-48e6-ad47-80eb92ddaa3d.png) + +### 腾河 + +腾讯是河南腾河网络科技有限公司的最大股东,第二大股东是河南日报。 + +主要做的业务方向是河南城市生活第一网。 + +工资比较低,另外,后端这块好像只招 PHP。 + +### 真二网 + +不得不说,这个名字有点东西! + +真二网创立于 2014 年,也是郑州本土的一家互联网公司,主要做 C2C 模式的 0 中介费真实二手房交易平台。 + +工资比较低,另外,后端这块好像只招 PHP。 + +### 硕诺科技 + +硕诺科技创立于 2014 年,总部位于上海,主要做物流软件系统高端定制的软件开发。 + +网上可以查到的消息比较少,感兴趣的小伙伴可以自己去查一下相关信息啊! + +另外,如果有小伙伴对这家公司比较了解,也可以在评论区说一下啊! + +### 科大讯飞 + +科大讯飞在郑州金水区有一个小分部,大部分招聘的都是和技术无关的岗位,不过也有一个 Java 开发岗。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-dc1517e7-c93b-457f-96d2-0871c17bf916.png) + +### 字节跳动 + +郑州也有字节跳动分部,不过都是和市场拓展与运营相关的岗位,像技术开发岗是没有的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-c7d11e68-2b85-4895-a3c5-44f6816ccfb2.png) + +类似的还有美团、华为、阿里巴巴等大厂,这些公司在郑州招聘的基本也都是非技术岗位。 + +### 其他 + +其他还有像郑州点读电子科技有限公司(旗下产品有咿啦看书)、羲和网络(河南唯一一家游戏上市企业)、米宅(中国知名的楼市自媒体,新三板上市企业)等互联网公司,感兴趣的小伙伴可以自行查阅相关信息呀! + +读者补充:海康威视、APUS、云鸟、亚信科技、牧原食品、小鱼易联、神州信息、云智慧都在郑州招开发工程师。 + +## 生活环境&生活成本 + +我们再来看看生活环境和生活成本。 + +### 房价 + +郑州的房价对于其发展来说还是比较贵的。当然了,相比于一线城市肯定还是要便宜很多的! + +以下房价数据来源于安居客,可以作为参考。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-95ec5784-5064-488c-bce8-4c076115a021.png) + +### 教育 + +郑州的教育资源极度匮乏!据统计郑州一共有 65 所高校,其中,本科 26 所,专科 39 所。 + +不过,211 院校仅有一所——郑州大学。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-03294872-3914-441b-ae6b-55200bce097a.png) + +是的!作为偌大的河南省的省会,国家历史文化名城,也就只有一所 211! + +我们来对比一下湖北省省会武汉,武汉 7 所 985/211 高校,分别是武汉大学、华中科技大学、中国地质大学、武汉理工大学、华中师范大学、华中农业大学、中南财经政法大学。 + +再来对比一下湖南省省会长沙,长沙有 4 所 985/211 高校,分别是国防科大、中南大学、湖南大学、湖南师范大学。 + +### 医疗 + +郑州的每万人床位数排名比较靠前。 + +另外,郑州市的优质医疗资源,在金水区、中原区、管城区比较集中; + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-6bbd8d11-dbe5-43c4-9354-b422b7fe1f77.png) + +### 本地居民 + +网上有很多“河南黑”,让很多人对河南的影响不好! + +实际情况可能并不是这样的!郑州本地居民不排外,绝大部分都特别老实,本本分分。 + +我去过很多城市,郑州人的友善程度我觉得是可以排在 Top 级别的! + +另外,郑州这边的居民还是比较恋家的。有很多在北上广混的还不错的人,最后也还是选择回来! + +### 交通 + +作为一个北方内陆城市,郑州可以说是一个“交通枢纽”。从郑州出发坐高铁,你去国内大部分地方都非常方便。 + +下图中的部分高铁线路正在修建,比如郑万高铁全线大概是 2021 年中旬通车。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-0a4d6b3c-a756-4509-a6fd-f3a65f018c84.png) + +郑州的地铁规划情况如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-264ab1f2-f355-466a-ab5d-2dc21668e888.gif) + +目前的话,郑州地铁有 1 号线、2 号线、3 号线、4 号线、城郊线、5 号线、14 号线一期 7 条地铁线。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-c5e61f03-efcb-4e56-b9bf-ff7bcec3dce0) + +### 美食 + +郑州的各种商业设施还是比较齐全的,有很多大型的商场,商场里面基本是样样俱全。 + +郑州好吃的还挺多的!去了郑州之后,一定要去喝胡辣汤,真的不要太好喝! + +![](http://cdn.tobebetterjavaer.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) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/collection/PriorityQueue.md b/docs/collection/PriorityQueue.md new file mode 100644 index 0000000000..0e9ba8721e --- /dev/null +++ b/docs/collection/PriorityQueue.md @@ -0,0 +1,191 @@ +--- +title: 详解 Java 中的优先级队列(PriorityQueue 附源码分析) +shortTitle: 详解PriorityQueue +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,详解 Java 中的优先级队列(PriorityQueue 附源码分析) +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,优先级队列,PriorityQueue +--- + +Java 中的 PriorityQueue 事通过二叉小顶堆实现的,可以用一棵完全二叉树表示。本文从 Queue 接口出发,结合生动的图解,深入浅出地分析 PriorityQueue 每个操作的具体过程和时间复杂度,让读者对 PriorityQueue 建立清晰而深入的认识。 + +## 总体介绍 + +前面以 Java [ArrayDeque](https://tobebetterjavaer.com/collection/arraydeque.html)为例讲解了*Stack*和*Queue*,其实还有一种特殊的队列叫做*PriorityQueue*,即优先队列。 + +**优先队列的作用是能保证每次取出的元素都是队列中权值最小的**(Java 的优先队列每次取最小元素,C++的优先队列每次取最大元素)。 + +这里牵涉到了大小关系,**元素大小的评判可以通过元素本身的自然顺序(_natural ordering_),也可以通过构造时传入的比较器**(_Comparator_,类似于 C++的仿函数)。 + +Java 中*PriorityQueue*实现了*Queue*接口,不允许放入`null`元素;其通过堆实现,具体说是通过完全二叉树(_complete binary tree_)实现的**小顶堆**(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为*PriorityQueue*的底层实现。 + +![PriorityQueue_base.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-8dca2f55-a7c7-49e1-95a5-df1a34f2aef5.png) + +上图中我们给每个元素按照层序遍历的方式进行了编号,如果你足够细心,会发现父节点和子节点的编号是有联系的,更确切的说父子节点的编号之间有如下关系: + +``` +leftNo = parentNo\*2+1 + +rightNo = parentNo\*2+2 + +parentNo = (nodeNo-1)/2 +``` + +通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。 + +*PriorityQueue*的`peek()`和`element`操作是常数时间,`add()`, `offer()`, 无参数的`remove()`以及`poll()`方法的时间复杂度都是*log(N)*。 + +## 方法剖析 + +### add()和 offer() + +`add(E e)`和`offer(E e)`的语义相同,都是向优先队列中插入元素,只是`Queue`接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回`false`。对于*PriorityQueue*这两个方法其实没什么差别。 + +![PriorityQueue_offer.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-0fb89aa7-c8fa-4fad-adbb-40c61c3bb0e9.png) + +新加入的元素可能会破坏小顶堆的性质,因此需要进行必要的调整。 + +```Java +//offer(E e) +public boolean offer(E e) { + if (e == null)//不允许放入null元素 + throw new NullPointerException(); + modCount++; + int i = size; + if (i >= queue.length) + grow(i + 1);//自动扩容 + size = i + 1; + if (i == 0)//队列原来为空,这是插入的第一个元素 + queue[0] = e; + else + siftUp(i, e);//调整 + return true; +} +``` + +上述代码中,扩容函数`grow()`类似于`ArrayList`里的`grow()`函数,就是再申请一个更大的数组,并将原数组的元素复制过去,这里不再赘述。需要注意的是`siftUp(int k, E x)`方法,该方法用于插入元素`x`并维持堆的特性。 + +```Java +//siftUp() +private void siftUp(int k, E x) { + while (k > 0) { + int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2 + Object e = queue[parent]; + if (comparator.compare(x, (E) e) >= 0)//调用比较器的比较方法 + break; + queue[k] = e; + k = parent; + } + queue[k] = x; +} +``` + +新加入的元素`x`可能会破坏小顶堆的性质,因此需要进行调整。调整的过程为:**从`k`指定的位置开始,将`x`逐层与当前点的`parent`进行比较并交换,直到满足`x >= queue[parent]`为止**。注意这里的比较可以是元素的自然顺序,也可以是依靠比较器的顺序。 + +### element()和 peek() + +`element()`和`peek()`的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回`null`。根据小顶堆的性质,堆顶那个元素就是全局最小的那个;由于堆用数组表示,根据下标关系,`0`下标处的那个元素既是堆顶元素。所以**直接返回数组`0`下标处的那个元素即可**。 + +![PriorityQueue_peek.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-5059f157-845e-4d1c-b993-5cfe539d5607.png) + +代码也就非常简洁: + +```Java +//peek() +public E peek() { + if (size == 0) + return null; + return (E) queue[0];//0下标处的那个元素就是最小的那个 +} +``` + +### remove()和 poll() + +`remove()`和`poll()`方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回`null`。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。 + +![PriorityQueue_poll.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-e25ba931-2e6f-4c17-84b8-9b959733d541.png) + +代码如下: + +```Java +public E poll() { + if (size == 0) + return null; + int s = --size; + modCount++; + E result = (E) queue[0];//0下标处的那个元素就是最小的那个 + E x = (E) queue[s]; + queue[s] = null; + if (s != 0) + siftDown(0, x);//调整 + return result; +} +``` + +上述代码首先记录`0`下标处的元素,并用最后一个元素替换`0`下标位置的元素,之后调用`siftDown()`方法对堆进行调整,最后返回原来`0`下标处的那个元素(也就是最小的那个元素)。重点是`siftDown(int k, E x)`方法,该方法的作用是**从`k`指定的位置开始,将`x`逐层向下与当前点的左右孩子中较小的那个交换,直到`x`小于或等于左右孩子中的任何一个为止**。 + +```Java +//siftDown() +private void siftDown(int k, E x) { + int half = size >>> 1; + while (k < half) { + //首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标 + int child = (k << 1) + 1;//leftNo = parentNo*2+1 + Object c = queue[child]; + int right = child + 1; + if (right < size && + comparator.compare((E) c, (E) queue[right]) > 0) + c = queue[child = right]; + if (comparator.compare(x, (E) c) <= 0) + break; + queue[k] = c;//然后用c取代原来的值 + k = child; + } + queue[k] = x; +} +``` + +### remove(Object o) + +`remove(Object o)`方法用于删除队列中跟`o`相等的某一个元素(如果有多个相等,只删除一个),该方法不是*Queue*接口内的方法,而是*Collection*接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。具体来说,`remove(Object o)`可以分为 2 种情况:1. 删除的是最后一个元素。直接删除即可,不需要调整。2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用一次`siftDown()`即可。此处不再赘述。 + +![PriorityQueue_remove2.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-ed0d08d3-b38e-44a1-a710-ee7a01afda62.png) + +具体代码如下: + +```Java +//remove(Object o) +public boolean remove(Object o) { + //通过遍历数组的方式找到第一个满足o.equals(queue[i])元素的下标 + int i = indexOf(o); + if (i == -1) + return false; + int s = --size; + if (s == i) //情况1 + queue[i] = null; + else { + E moved = (E) queue[s]; + queue[s] = null; + siftDown(i, moved);//情况2 + ...... + } + return true; +} +``` + +> 参考链接:[https://github.com/CarpenterLee/JCFInternals](https://github.com/CarpenterLee/JCFInternals),作者:李豪,整理:沉默王二 + + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/WeakHashMap.md b/docs/collection/WeakHashMap.md similarity index 84% rename from docs/src/collection/WeakHashMap.md rename to docs/collection/WeakHashMap.md index c9fe6ffff7..62ca0e82d9 100644 --- a/docs/src/collection/WeakHashMap.md +++ b/docs/collection/WeakHashMap.md @@ -5,15 +5,15 @@ category: - Java核心 tag: - 集合框架(容器) -description: 二哥的Java进阶之路,小白的零基础Java教程,Java WeakHashMap详解(附源码分析) +description: Java程序员进阶之路,小白的零基础Java教程,Java WeakHashMap详解(附源码分析) head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,WeakHashMap + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,WeakHashMap --- -在Java中,我们一般都会使用到Map,比如[HashMap](https://javabetter.cn/collection/hashmap.html)这样的具体实现。更高级一点,我们可能会使用WeakHashMap。 +在Java中,我们一般都会使用到Map,比如[HashMap](https://tobebetterjavaer.com/collection/hashmap.html)这样的具体实现。更高级一点,我们可能会使用WeakHashMap。 WeakHashMap其实和HashMap大多数行为是一样的,只是WeakHashMap不会阻止GC回收key对象(不是value),那么WeakHashMap是怎么做到的呢,这就是我们研究的主要问题。 @@ -154,9 +154,8 @@ I/MainActivity(10202): dumpWeakInfo weakInfo.size=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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/arraydeque.md b/docs/collection/arraydeque.md new file mode 100644 index 0000000000..cdacb68323 --- /dev/null +++ b/docs/collection/arraydeque.md @@ -0,0 +1,206 @@ +--- +title: 详解 Java 中的堆和队列(ArrayDeque附源码分析) +shortTitle: 详解ArrayDeque +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,详解 Java 中的堆和队列(Stack and Queue 附源码分析) +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,ArrayDeque,堆,队列 +--- + +Java 里有一个叫做*Stack*的类,却没有叫做*Queue*的类(它是个接口名字)。当需要使用栈时,Java 已不推荐使用*Stack*,而是推荐使用更高效的*ArrayDeque*;既然*Queue*只是一个接口,当需要使用队列时也就首选*ArrayDeque*了(次选是[LinkedList](https://tobebetterjavaer.com/collection/linkedlist.html))。 + +## 总体介绍 + +要讲栈和队列,首先要讲*Deque*接口。*Deque*的含义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。下表列出了*Deque*与*Queue*相对应的接口: + +| Queue Method | Equivalent Deque Method | 说明 | +| ------------ | ----------------------- | -------------------------------------- | +| add(e) | addLast(e) | 向队尾插入元素,失败则抛出异常 | +| offer(e) | offerLast(e) | 向队尾插入元素,失败则返回`false` | +| remove() | removeFirst() | 获取并删除队首元素,失败则抛出异常 | +| poll() | pollFirst() | 获取并删除队首元素,失败则返回`null` | +| element() | getFirst() | 获取但不删除队首元素,失败则抛出异常 | +| peek() | peekFirst() | 获取但不删除队首元素,失败则返回`null` | + +下表列出了*Deque*与*Stack*对应的接口: + +| Stack Method | Equivalent Deque Method | 说明 | +| ------------ | ----------------------- | -------------------------------------- | +| push(e) | addFirst(e) | 向栈顶插入元素,失败则抛出异常 | +| 无 | offerFirst(e) | 向栈顶插入元素,失败则返回`false` | +| pop() | removeFirst() | 获取并删除栈顶元素,失败则抛出异常 | +| 无 | pollFirst() | 获取并删除栈顶元素,失败则返回`null` | +| peek() | peekFirst() | 获取但不删除栈顶元素,失败则抛出异常 | +| 无 | peekFirst() | 获取但不删除栈顶元素,失败则返回`null` | + +上面两个表共定义了*Deque*的 12 个接口。 + +添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。 + +**一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值(`false`或`null`)**。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。 + +**虽然*Deque*的接口有 12 个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看**。明白了这一点讲解起来就会非常简单。 + +*ArrayDeque*和*LinkedList*是*Deque*的两个通用实现,由于官方更推荐使用*AarryDeque*用作栈和队列,加之上一篇已经讲解过[LinkedList](https://tobebetterjavaer.com/collection/linkedlist.html),本文将着重讲解*ArrayDeque*的具体实现。 + +从名字可以看出*ArrayDeque*底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即**循环数组(circular array)**,也就是说数组的任何一点都可能被看作起点或者终点。 + +*ArrayDeque*是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要手动同步;另外,该容器不允许放入`null`元素。 + +![ArrayDeque_base.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-1e7086a3-3d31-4553-aa16-5eaf2193649e.png) + + +上图中我们看到,**`head`指向首端第一个有效元素,`tail`指向尾端第一个可以插入元素的空位**。因为是循环数组,所以`head`不一定总等于 0,`tail`也不一定总是比`head`大。 + +## 方法剖析 + +### addFirst() + +`addFirst(E e)`的作用是在*Deque*的首端插入元素,也就是在`head`的前面插入元素,在空间足够且下标没有越界的情况下,只需要将`elements[--head] = e`即可。 + + +![ArrayDeque_addFirst.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-459afbba-2778-4241-97fb-f01a29b79458.png) + +实际需要考虑: + +1. 空间是否够用,以及 +2. 下标是否越界的问题。 + +上图中,如果`head`为`0`之后接着调用`addFirst()`,虽然空余空间还够用,但`head`为`-1`,下标越界了。下列代码很好的解决了这两个问题。 + + +```java +//addFirst(E e) +public void addFirst(E e) { + if (e == null)//不允许放入null + throw new NullPointerException(); + elements[head = (head - 1) & (elements.length - 1)] = e;//2.下标是否越界 + if (head == tail)//1.空间是否够用 + doubleCapacity();//扩容 +} +``` + +``` +//addFirst(E e) +public void addFirst(E e) { + if (e == null)//不允许放入null + throw new NullPointerException(); + elements\[head = (head - 1) & (elements.length - 1)\] = e;//2.下标是否越界 + if (head == tail)//1.空间是否够用 + doubleCapacity();//扩容 +} +``` + +上述代码我们看到,**空间问题是在插入之后解决的**,因为`tail`总是指向下一个可插入的空位,也就意味着`elements`数组至少有一个空位,所以插入元素的时候不用考虑空间问题。 + +下标越界的处理解决起来非常简单,`head = (head - 1) & (elements.length - 1)`就可以了,**这段代码相当于取余,同时解决了`head`为负值的情况**。因为`elements.length`必需是`2`的指数倍,`elements - 1`就是二进制低位全`1`,跟`head - 1`相与之后就起到了取模的作用,如果`head - 1`为负数(其实只可能是-1),则相当于对其取相对于`elements.length`的补码。 + +下面再说说扩容函数`doubleCapacity()`,其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示: + +![ArrayDeque_doubleCapacity.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-f1386b63-10be-4998-bb6d-bf6560cca7ee.png) + +图中我们看到,复制分两次进行,第一次复制`head`右边的元素,第二次复制`head`左边的元素。 + +``` +//doubleCapacity() +private void doubleCapacity() { + assert head == tail; + int p = head; + int n = elements.length; + int r = n - p; // head右边元素的个数 + int newCapacity = n << 1;//原空间的2倍 + if (newCapacity < 0) + throw new IllegalStateException("Sorry, deque too big"); + Object\[\] a = new Object\[newCapacity\]; + System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分 + System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分 + elements = (E\[\])a; + head = 0; + tail = n; +} +``` + +### addLast() + +`addLast(E e)`的作用是在*Deque*的尾端插入元素,也就是在`tail`的位置插入元素,由于`tail`总是指向下一个可以插入的空位,因此只需要`elements[tail] = e;`即可。插入完成后再检查空间,如果空间已经用光,则调用`doubleCapacity()`进行扩容。 + +![ArrayDeque_addLast.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-832c796a-6c24-4546-9f91-22ed39884363.png) + +``` +public void addLast(E e) { + if (e == null)//不允许放入null + throw new NullPointerException(); + elements\[tail\] = e;//赋值 + if ( (tail = (tail + 1) & (elements.length - 1)) == head)//下标越界处理 + doubleCapacity();//扩容 +} +``` + +下标越界处理方式`addFirt()`中已经讲过,不再赘述。 + +### pollFirst() + +`pollFirst()`的作用是删除并返回*Deque*首端元素,也即是`head`位置处的元素。如果容器不空,只需要直接返回`elements[head]`即可,当然还需要处理下标的问题。由于`ArrayDeque`中不允许放入`null`,当`elements[head] == null`时,意味着容器为空。 + +``` +public E pollFirst() { + E result = elements\[head\]; + if (result == null)//null值意味着deque为空 + return null; + elements\[h\] = null;//let GC work + head = (head + 1) & (elements.length - 1);//下标越界处理 + return result; +} +``` + +### pollLast() + +`pollLast()`的作用是删除并返回*Deque*尾端元素,也即是`tail`位置前面的那个元素。 + +``` +public E pollLast() { + int t = (tail - 1) & (elements.length - 1);//tail的上一个位置是最后一个元素 + E result = elements\[t\]; + if (result == null)//null值意味着deque为空 + return null; + elements\[t\] = null;//let GC work + tail = t; + return result; +} +``` + +### peekFirst() + +`peekFirst()`的作用是返回但不删除*Deque*首端元素,也即是`head`位置处的元素,直接返回`elements[head]`即可。 + +``` +public E peekFirst() { + return elements\[head\]; // elements\[head\] is null if deque empty +} +``` + +### peekLast() + +`peekLast()`的作用是返回但不删除*Deque*尾端元素,也即是`tail`位置前面的那个元素。 + +``` +public E peekLast() { + return elements\[(tail - 1) & (elements.length - 1)\]; +} +``` + +>参考链接:[https://github.com/CarpenterLee/JCFInternals](https://github.com/CarpenterLee/JCFInternals),作者:李豪,整理:沉默王二 + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/arraylist.md b/docs/collection/arraylist.md new file mode 100644 index 0000000000..c4f1b7df4c --- /dev/null +++ b/docs/collection/arraylist.md @@ -0,0 +1,416 @@ +--- +title: Java ArrayList详解(附源码分析) +shortTitle: ArrayList详解 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java ArrayList详解 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,ArrayList,ArrayList源码 +--- + +“二哥,听说今天我们开讲 ArrayList 了?好期待哦!”三妹明知故问,这个托配合得依然天衣无缝。 + +“是的呀,三妹。”我肯定地点了点头,继续说道,“ArrayList 可以称得上是集合框架方面最常用的类了,可以和 HashMap 一较高下。” + +从名字就可以看得出来,ArrayList 实现了 List 接口,并且是基于数组实现的。 + +数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。ArrayList 在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。 + +Java 这门编程语言和 C语言的不同之处就在这里,如果是 C语言的话,就必须动手实现自己的 ArrayList,原生的库函数里面是没有的。 + +## 创建 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 中添加元素 + +“二哥,**那怎么向 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()`,它会对数组进行复制(要插入位置上的元素往后复制)。 + +“三妹,注意看,我画幅图来表示下。”我认真地做起了图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraylist-01.png) + +## 更新 ArrayList 中的元素 + + +“二哥,那怎么**更新 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 中的元素 + +“二哥,那怎么**删除 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()` 方法对数组进行复制和移动。 + +“三妹,注意看,我画幅图来表示下。”我认真地做起了图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraylist-02.png) + +## 查找 ArrayList 中的元素 + +“二哥,那怎么**查找 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 增删改查的时间复杂度 + +“最后,三妹,我来简单总结一下 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`,有些初学者会选择跳过,虽然不影响整体上的学习,但也错过了一个精进的机会。 + +计算机内部是如何表示十进制数的,右移时又发生了什么,静下心来去研究一下,你就会发现,哦,原来这么有趣呢? + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/fail-fast.md b/docs/collection/fail-fast.md new file mode 100644 index 0000000000..195ccf08c8 --- /dev/null +++ b/docs/collection/fail-fast.md @@ -0,0 +1,263 @@ +--- +title: 为什么阿里巴巴强制不要在foreach里执行删除操作?还不是因为fail-fast +shortTitle: 为什么不要在foreach里执行删除操作? +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,为什么阿里巴巴强制不要在foreach里执行删除操作?还不是因为fail-fast +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,fail-fast +--- + + +那天,小二去阿里面试,面试官老王一上来就甩给了他一道面试题:为什么阿里的 Java 开发手册里会强制不要在 foreach 里进行元素的删除操作? + +小二听完这句话就乐了。为什么呢?因为一天前他刚在《Java 程序员进阶之路》上看到过这道题的答案。 + +以下是整篇文章的内容。 + +----- + +为了镇楼,先搬一段英文来解释一下 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); +``` + +这段代码看起来没有任何问题,但运行起来就报错了。 + +![](http://cdn.tobebetterjavaer.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](戳链接🔗,详细了解下) 配合 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 方式。 + +![](http://cdn.tobebetterjavaer.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, 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 本质上是一种语法糖,遍历集合时很方面,但并不适合拿来操作集合中的元素(增删)。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/gailan.md b/docs/collection/gailan.md new file mode 100644 index 0000000000..60616e4433 --- /dev/null +++ b/docs/collection/gailan.md @@ -0,0 +1,211 @@ +--- +title: Java 集合框架(容器)体系结构 +shortTitle: Java集合框架体系结构 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java 集合框架(容器)体系结构 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,集合框架,容器 +--- + + +眼瞅着三妹的王者荣耀杀得正嗨,我趁机喊到:“别打了,三妹,我们来一起学习 Java 的集合框架吧。” + +“才不要呢,等我打完这一局啊。”三妹倔强地说。 + +“好吧。”我只好摊摊手地说,“那我先画张集合框架的结构图等着你。” + +![](http://cdn.tobebetterjavaer.com/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 个元素的双端队列。 + +![](http://cdn.tobebetterjavaer.com/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。 + +于是 LinkedHashMap 就闪亮登场了。LinkedHashMap 是 HashMap 的子类,内部使用链表来记录插入/访问元素的顺序。 + +LinkedHashMap 可以看作是 HashMap + LinkedList 的合体,它使用了 哈希表来存储数据,又用了双向链表来维持顺序。 + +### **3)TreeMap** + +HashMap 是无序的,所以遍历的时候元素的顺序也是不可测的。TreeMap 是有序的,它在内部会对键进行排序,所以遍历的时候就可以得到预期的顺序。 + +为了保证顺序,TreeMap 的键必须要实现 Comparable 接口或者 Comparator 接口。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/collection/hashmap.md b/docs/collection/hashmap.md new file mode 100644 index 0000000000..003a2807bc --- /dev/null +++ b/docs/collection/hashmap.md @@ -0,0 +1,816 @@ +--- +title: Java HashMap详解(附源码分析) +shortTitle: HashMap详解 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java HashMap详解 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,HashMap +--- + +这篇文章将通过源码的方式,详细透彻地讲清楚 Java 的 HashMap,包括HashMap hash 方法的原理、HashMap 的扩容机制、HashMap的加载因子为什么是 0.75 而不是 0.6、0.8,以及 HashMap 为什么是线程不安全的,所有 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 亿的映射空间,只要哈希值映射得比较均匀松散,一般是不会出现哈希碰撞的。 + +>PS:读者建议范围加上 左闭右开。因为 int 类型为 4字节,也就是 32位,取值范围为 `[-2^31,2^31-1]`。也就是 -2147483648 到 2147483647 + +但问题是一个 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 方法呢? + +看下面这个图。 + +![](http://cdn.tobebetterjavaer.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 方法就是为了增加随机性,让数据元素更加均衡的分布,减少碰撞**。 + +## 二、扩容机制 + +大家都知道,数组一旦初始化后大小就无法改变了,所以就有了 [ArrayList](https://tobebetterjavaer.com/collection/arraylist.html)这种“动态数组”,可以自动扩容。 + +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 算法就是简单的用键的哈希值(一个 int 值)和数组大小取模(也就是 hashCode % table.length)。 + +继续假设: + +- 数组 table 的长度为 2 +- 键的哈希值为 3、7、5 + +取模运算后,哈希冲突都到 table[1] 上了,因为余数为 1。那么扩容前的样子如下图所示。 + +![](http://cdn.tobebetterjavaer.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] 上。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-resize-02.png) + +按照我们的预期,扩容后的 7 仍然应该在 3 这条链表的后面,但实际上呢? 7 跑到 3 这条链表的头部了。针对 JDK 7 中的这个情况,JDK 8 做了哪些优化呢? + +看下面这张图。 + +![](http://cdn.tobebetterjavaer.com/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,也就是原来的索引+原来的容量 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-resize-04.png) + + +也就是说,JDK 8 不需要像 JDK 7 那样重新计算 hash,只需要看原来的hash值新增的那个bit是1还是0就好了,是0的话就表示索引没变,是1的话,索引就变成了“原索引+原来的容量”。 + +![](http://cdn.tobebetterjavaer.com/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; +} +``` + +## 三、加载因子为什么是0.75 + +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 的数组长度达到一个**临界值**的时候,就会触发扩容,扩容后会将之前小数组中的元素转移到大数组中,这是一个相当耗时的操作。 + +这个临界值由什么来确定呢? + +>临界值 = 初始容量 * 加载因子 + +一开始,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) + +具体是用这么一个公式来表示的。 + +![](http://cdn.tobebetterjavaer.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 +``` + +大致的意思就是: + +因为 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](https://segmentfault.com/a/1190000023308658) + +里面提到了一个概念:**二项分布**(二哥概率论没学好,只能简单说一说)。 + +在做一件事情的时候,其结果的概率只有2种情况,和抛硬币一样,不是正面就是反面。 + +为此,我们做了 N 次实验,那么在每次试验中只有两种可能的结果,并且每次实验是独立的,不同实验之间互不影响,每次实验成功的概率都是一样的。 + +以此理论为基础,我们来做这样的实验:我们往哈希表中扔数据,如果发生哈希冲突就为失败,否则为成功。 + +我们可以设想,实验的hash值是随机的,并且经过hash运算的键都会映射到hash表的地址空间上,那么这个结果也是随机的。所以,每次put的时候就相当于我们在扔一个16面(我们先假设默认长度为16)的骰子,扔骰子实验那肯定是相互独立的。碰撞发生即扔了n次有出现重复数字。 + +然后,我们的目的是啥呢? + +就是掷了k次骰子,没有一次是相同的概率,需要尽可能的大些,一般意义上我们肯定要大于0.5(这个数是个理想数,但是我是能接受的)。 + +于是,n次事件里面,碰撞为0的概率,由上面公式得: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-02.png) + +这个概率值需要大于0.5,我们认为这样的hashmap可以提供很低的碰撞率。所以: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-03png) + +这时候,我们对于该公式其实最想求的时候长度s的时候,n为多少次就应该进行扩容了?而负载因子则是$n/s$的值。所以推导如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-04.png) + +所以可以得到 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-05.png) + +其中 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-06.png) + +这就是一个求 `∞⋅0`函数极限问题,这里我们先令$s = m+1(m \to \infty)$则转化为 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-07.png) + +我们再令 $x = \frac{1}{m} (x \to 0)$ 则有, + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-08.png) + +所以, + +![](http://cdn.tobebetterjavaer.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 是个比较完美的选择。 + +## 四、线程不安全 + +三方面原因:多线程下扩容会死循环、多线程下 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` 这两行代码,就会将同一位置上的新元素被放在链表的头部。 + +扩容前的样子假如是下面这样子。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-01.png) + +那么正常扩容后就是下面这样子。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-02.png) + +假设现在有两个线程同时进行扩容,线程 A 在执行到 `newTable[i] = e;` 被挂起,此时线程 A 中:e=3、next=7、e.next=null + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-03.png) + + +线程 B 开始执行,并且完成了数据转移。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-04.png) + + +此时,7 的 next 为 3,3 的 next 为 null。 + +随后线程A获得CPU时间片继续执行 `newTable[i] = e`,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下: + +![](http://cdn.tobebetterjavaer.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 了。 + +采用头部插入的方式,变成了下面这样子: + +![](http://cdn.tobebetterjavaer.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 被头插法重新插入到链表中,执行结果如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-07.png) + +套娃开始,元素 5 也就成了弃婴,惨~~~ + +不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序。 + +### 02、多线程下 put 会导致元素丢失 + +正常情况下,当发生哈希冲突时,HashMap 是这样的: + +![](http://cdn.tobebetterjavaer.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 是这样的: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-09.png) + +接着,线程 B 执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的: + +![](http://cdn.tobebetterjavaer.com/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 了,因为元素还没有转移。 + +参考链接: + +> - [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) + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/iterator-iterable.md b/docs/collection/iterator-iterable.md new file mode 100644 index 0000000000..da2d7278fa --- /dev/null +++ b/docs/collection/iterator-iterable.md @@ -0,0 +1,262 @@ +--- +title: 海康威视一面:Java中Iterator和Iterable有什么区别? +shortTitle: Iterator和Iterable有什么区别? +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java中Iterator和Iterable有什么区别? +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Iterable,Iterator +--- + + +那天,小二去海康威视面试,面试官老王一上来就甩给了他一道面试题:请问 Iterator与Iterable有什么区别? + +小二表示很开心,因为他3 天前刚好在《Java 程序员进阶之路》上读过这篇文章,所以回答得胸有成竹。 + +以下↓是文章的内容。 + +---- + +在 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 的影子。 + +![](http://cdn.tobebetterjavaer.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 +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我们就先聊这么多,总结两点: + +- 学会深入思考,一点点抽丝剥茧,多想想为什么这样实现,很多问题没有自己想象中的那么复杂。 +- 遇到疑惑不放弃,这是提升自己最好的机会,遇到某个疑难的点,解决的过程中会挖掘出很多相关的东西。 + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/linkedhashmap.md b/docs/collection/linkedhashmap.md new file mode 100644 index 0000000000..88cc074d2d --- /dev/null +++ b/docs/collection/linkedhashmap.md @@ -0,0 +1,351 @@ +--- +title: Java LinkedHashMap详解(附源码分析) +shortTitle: LinkedHashMap详解 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java LinkedHashMap详解 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,LinkedHashMap +--- + +俗话说了,“金无足赤人无完人”,HashMap 也不例外,有一种需求它就满足不了,假如我们需要一个按照插入顺序来排列的键值对集合,那 HashMap 就无能为力了。那该怎么办呢?必须得上今天这篇文章的主角:LinkedHashMap。 + +同学们好啊,还记得 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 那篇吗?我自己感觉写得非常棒啊,既通俗易懂,又深入源码,真的是分析得透透彻彻、清清楚楚、明明白白的。(一不小心又甩了三个成语,有文化吧?)HashMap 哪哪都好,真的,只要你想用键值对,第一时间就应该想到它。 + +为了提高查找效率,HashMap 在插入的时候对键做了一次哈希算法,这就导致插入的元素是无序的。 + +对这一点还不太明白的同学,可以再回到 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 那一篇,看看我对 `put()` 方法的讲解。 + +``` +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + +               boolean evict) { +    HashMap.Node[] tab; HashMap.Node p; int n, i; +    // ①、数组 table 为 null 时,调用 resize 方法创建默认大小的数组 +    if ((tab = table) == null || (n = tab.length) == 0) +        n = (tab = resize()).length; +    // ②、计算下标,如果该位置上没有值,则填充 +    if ((p = tab[i = (n - 1) & hash]) == null) +        tab[i] = newNode(hash, key, value, null); +} +``` + +这个公式 `i = (n - 1) & hash` 计算后的值并不是按照 0、1、2、3、4、5 这样有序的下标将键值对插入到数组当中的,而是有一定的随机性。 + +那 LinkedHashMap 就是为这个需求应运而生的。LinkedHashMap 继承了 HashMap,所以 HashMap 有的关于键值对的功能,它也有了。 + +``` +public class LinkedHashMap + +    extends HashMap + +    implements Map{} +``` + +此外,LinkedHashMap 内部又追加了双向链表,来维护元素的插入顺序。注意下面代码中的 before 和 after,它俩就是用来维护当前元素的前一个元素和后一个元素的顺序的。 + +``` +static class Entry extends HashMap.Node { +    LinkedHashMap.Entry before, after; +    Entry(int hash, K key, V value, HashMap.Node next) { +        super(hash, key, value, next); +    } +} +``` + +关于双向链表,同学们可以回头看一遍我写的 [LinkedList](https://tobebetterjavaer.com/collection/linkedlist.html) 那篇文章,会对理解本篇的 LinkedHashMap 有很大的帮助。 + +## 01、插入顺序 + +在 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 那篇文章里,我有讲解到一点,不知道同学们记不记得,就是 null 会插入到 HashMap 的第一位。 + +``` +Map hashMap = new HashMap<>(); +hashMap.put("沉", "沉默王二"); +hashMap.put("默", "沉默王二"); +hashMap.put("王", "沉默王二"); +hashMap.put("二", "沉默王二"); +hashMap.put(null, null); + +for (String key : hashMap.keySet()) { +    System.out.println(key + " : " + hashMap.get(key)); +} +``` + +输出的结果是: + +``` +null : null +默 : 沉默王二 +沉 : 沉默王二 +王 : 沉默王二 +二 : 沉默王二 +``` + +虽然 null 最后一位 put 进去的,但在遍历输出的时候,跑到了第一位。 + +那再来对比看一下 LinkedHashMap。 + +``` +Map linkedHashMap = new LinkedHashMap<>(); +linkedHashMap.put("沉", "沉默王二"); +linkedHashMap.put("默", "沉默王二"); +linkedHashMap.put("王", "沉默王二"); +linkedHashMap.put("二", "沉默王二"); +linkedHashMap.put(null, null); + +for (String key : linkedHashMap.keySet()) { +    System.out.println(key + " : " + linkedHashMap.get(key)); +} +``` + +输出结果是: + +``` +沉 : 沉默王二 +默 : 沉默王二 +王 : 沉默王二 +二 : 沉默王二 +null : null +``` + +null 在最后一位插入,在最后一位输出。 + +输出结果可以再次证明,HashMap 是无序的,LinkedHashMap 是可以维持插入顺序的。 + +那 LinkedHashMap 是如何做到这一点呢?我相信同学们和我一样,非常希望知道原因。 + +要想搞清楚,就需要深入研究一下 LinkedHashMap 的源码。LinkedHashMap 并未重写 HashMap 的 `put()` 方法,而是重写了 `put()` 方法需要调用的内部方法 `newNode()`。 + +``` +HashMap.Node newNode(int hash, K key, V value, HashMap.Node e) { +    LinkedHashMap.Entry p = +            new LinkedHashMap.Entry<>(hash, key, value, e); +    linkNodeLast(p); +    return p; +} +``` + +前面说了,LinkedHashMap.Entry 继承了 HashMap.Node,并且追加了两个字段 before 和 after。 + +那,紧接着来看看 `linkNodeLast()` 方法: + +``` +private void linkNodeLast(LinkedHashMap.Entry p) { +    LinkedHashMap.Entry last = tail; +    tail = p; +    if (last == null) +        head = p; +    else { +        p.before = last; +        last.after = p; +    } +} +``` + +看到了吧,LinkedHashMap 在添加第一个元素的时候,会把 head 赋值为第一个元素,等到第二个元素添加进来的时候,会把第二个元素的 before 赋值为第一个元素,第一个元素的 afer 赋值为第二个元素。 + +这就保证了键值对是按照插入顺序排列的,明白了吧? + +*注:这篇文章当时用到的 JDK 版本为 14(当时的最新版,建议使用 Java8 或者 Java 13)*。 + +## 02、访问顺序 + +LinkedHashMap 不仅能够维持插入顺序,还能够维持访问顺序。访问包括调用 `get()` 方法、`remove()` 方法和 `put()` 方法。 + +要维护访问顺序,需要我们在声明 LinkedHashMap 的时候指定三个参数。 + +``` +LinkedHashMap map = new LinkedHashMap<>(16, .75f, true); +``` + +第一个参数和第二个参数,看过 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 的同学们应该很熟悉了,指的是初始容量和负载因子。 + +第三个参数如果为 true 的话,就表示 LinkedHashMap 要维护访问顺序;否则,维护插入顺序。默认是 false。 + +``` +Map linkedHashMap = new LinkedHashMap<>(16, .75f, true); +linkedHashMap.put("沉", "沉默王二"); +linkedHashMap.put("默", "沉默王二"); +linkedHashMap.put("王", "沉默王二"); +linkedHashMap.put("二", "沉默王二"); + +System.out.println(linkedHashMap); + +linkedHashMap.get("默"); +System.out.println(linkedHashMap); + +linkedHashMap.get("王"); +System.out.println(linkedHashMap); +``` + +输出的结果如下所示: + +``` +{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二} +{沉=沉默王二, 王=沉默王二, 二=沉默王二, 默=沉默王二} +{沉=沉默王二, 二=沉默王二, 默=沉默王二, 王=沉默王二} +``` + +当我们使用 `get()` 方法访问键位“默”的元素后,输出结果中,`默=沉默王二` 在最后;当我们访问键位“王”的元素后,输出结果中,`王=沉默王二` 在最后,`默=沉默王二` 在倒数第二位。 + +也就是说,最不经常访问的放在头部,这就有意思了。有意思在哪呢? + +我们可以使用 LinkedHashMap 来实现 LRU 缓存,LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 + +``` +public class MyLinkedHashMap extends LinkedHashMap { + +    private static final int MAX_ENTRIES = 5; + +    public MyLinkedHashMap( + +            int initialCapacity, float loadFactor, boolean accessOrder) { +        super(initialCapacity, loadFactor, accessOrder); +    } + +    @Override +    protected boolean removeEldestEntry(Map.Entry eldest) { +        return size() > MAX_ENTRIES; +    } + +} +``` + +MyLinkedHashMap 是一个自定义类,它继承了 LinkedHashMap,并且重写了 `removeEldestEntry()` 方法——使 Map 最多可容纳 5 个元素,超出后就淘汰。 + +我们来测试一下。 + +``` +MyLinkedHashMap map = new MyLinkedHashMap<>(16,0.75f,true); +map.put("沉", "沉默王二"); +map.put("默", "沉默王二"); +map.put("王", "沉默王二"); +map.put("二", "沉默王二"); +map.put("一枚有趣的程序员", "一枚有趣的程序员"); + +System.out.println(map); + +map.put("一枚有颜值的程序员", "一枚有颜值的程序员"); +System.out.println(map); + +map.put("一枚有才华的程序员","一枚有才华的程序员"); +System.out.println(map); +``` + +输出结果如下所示: + +``` +{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员} +{默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员, 一枚有颜值的程序员=一枚有颜值的程序员} +{王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员, 一枚有颜值的程序员=一枚有颜值的程序员, 一枚有才华的程序员=一枚有才华的程序员} +``` + +`沉=沉默王二` 和 `默=沉默王二` 依次被淘汰出局。 + +假如在 put “一枚有才华的程序员”之前 get 了键位为“默”的元素: + +``` +MyLinkedHashMap map = new MyLinkedHashMap<>(16,0.75f,true); +map.put("沉", "沉默王二"); +map.put("默", "沉默王二"); +map.put("王", "沉默王二"); +map.put("二", "沉默王二"); +map.put("一枚有趣的程序员", "一枚有趣的程序员"); + +System.out.println(map); + +map.put("一枚有颜值的程序员", "一枚有颜值的程序员"); +System.out.println(map); + +map.get("默"); +map.put("一枚有才华的程序员","一枚有才华的程序员"); +System.out.println(map); +``` + +那输出结果就变了,对吧? + +``` +{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员} +{默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员, 一枚有颜值的程序员=一枚有颜值的程序员} +{二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员, 一枚有颜值的程序员=一枚有颜值的程序员, 默=沉默王二, 一枚有才华的程序员=一枚有才华的程序员} +``` + +`沉=沉默王二` 和 `王=沉默王二` 被淘汰出局了。 + +那 LinkedHashMap 是如何来维持访问顺序呢?同学们感兴趣的话,可以研究一下下面这三个方法。 + +``` +void afterNodeAccess(Node p) { } +void afterNodeInsertion(boolean evict) { } +void afterNodeRemoval(Node p) { } +``` + +`afterNodeAccess()` 会在调用 `get()` 方法的时候被调用,`afterNodeInsertion()` 会在调用 `put()` 方法的时候被调用,`afterNodeRemoval()` 会在调用 `remove()` 方法的时候被调用。 + +我来以 `afterNodeAccess()` 为例来讲解一下。 + +``` +void afterNodeAccess(HashMap.Node e) { // move node to last +    LinkedHashMap.Entry last; +    if (accessOrder && (last = tail) != e) { +        LinkedHashMap.Entry p = +                (LinkedHashMap.Entry)e, b = p.before, a = p.after; +        p.after = null; +        if (b == null) +            head = a; +        else +            b.after = a; +        if (a != null) +            a.before = b; +        else +            last = b; +        if (last == null) +            head = p; +        else { +            p.before = last; +            last.after = p; +        } +        tail = p; +        ++modCount; +    } +} +``` + +哪个元素被 get 就把哪个元素放在最后。了解了吧? + +那同学们可能还想知道,为什么 LinkedHashMap 能实现 LRU 缓存,把最不经常访问的那个元素淘汰? + +在插入元素的时候,需要调用 `put()` 方法,该方法最后会调用 `afterNodeInsertion()` 方法,这个方法被 LinkedHashMap 重写了。 + +``` +void afterNodeInsertion(boolean evict) { // possibly remove eldest +    LinkedHashMap.Entry first; +    if (evict && (first = head) != null && removeEldestEntry(first)) { +        K key = first.key; +        removeNode(hash(key), key, null, false, true); +    } +} +``` + +`removeEldestEntry()` 方法会判断第一个元素是否超出了可容纳的最大范围,如果超出,那就会调用 `removeNode()` 方法对最不经常访问的那个元素进行删除。 + +## 03、最后 + +由于 LinkedHashMap 要维护双向链表,所以 LinkedHashMap 在插入、删除操作的时候,花费的时间要比 HashMap 多一些。 + +这也是没办法的事,对吧,欲戴皇冠必承其重嘛。既然想要维护元素的顺序,总要付出点代价才行。 + + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/linkedlist.md b/docs/collection/linkedlist.md new file mode 100644 index 0000000000..4a37488a29 --- /dev/null +++ b/docs/collection/linkedlist.md @@ -0,0 +1,398 @@ +--- +title: Java LinkedList详解(附源码分析) +shortTitle: LinkedList详解 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java LinkedList详解 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,LinkedList,LinkedList源码 +--- + + +## 一、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; + } +} +``` + +它由三部分组成: + +- 节点上的元素 +- 下一个节点 +- 上一个节点 + +我画幅图给你们展示下吧。 + +![](http://cdn.tobebetterjavaer.com/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。 + +此时还不能称之为链表,因为前后节点都是断裂的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/linkedlist-02.png) + +- 添加第二个元素的时候,first 和 last 都指向的是第一个节点。 +- 然后新建一个节点 newNode,它的 prev 指向的是第一个节点,next 为 null。 +- 然后把第一个节点的 next 赋值为 newNode。 + +此时的链表还不完整。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/linkedlist-03.png) + +- 添加第三个元素的时候,first 指向的是第一个节点,last 指向的是最后一个节点。 +- 然后新建一个节点 newNode,它的 prev 指向的是第二个节点,next 为 null。 +- 然后把第二个节点的 next 赋值为 newNode。 + +此时的链表已经完整了。 + +![](http://cdn.tobebetterjavaer.com/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 拿来比较,因为我们各自修炼的内功不同,没有孰高孰低。 + +虽然师兄经常喊我一声师弟,但我们之间其实挺和谐的。但我知道,在外人眼里,同门师兄弟,总要一较高下的。 + +比如说,我们俩在增删改查时候的时间复杂度。 + +也许这就是命运吧,从我进入师门的那天起,这种争论就一直没有停息过。 + +无论外人怎么看待我们,在我眼里,师兄永远都是一哥,我敬重他,他也愿意保护我。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/list-war-2.md b/docs/collection/list-war-2.md new file mode 100644 index 0000000000..2e54203ff8 --- /dev/null +++ b/docs/collection/list-war-2.md @@ -0,0 +1,854 @@ +--- +title: 面试官问我Java中ArrayList和LinkedList的区别,我和他扯了半小时 +shortTitle: ArrayList和LinkedList的区别 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java中ArrayList和LinkedList的区别 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,LinkedList,ArrayList +--- + + +## 01、ArrayList 是如何实现的? + +ArrayList 实现了 List 接口,继承了 AbstractList 抽象类。 + +![](http://cdn.tobebetterjavaer.com/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。 + +![](http://cdn.tobebetterjavaer.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、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)` 方法,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/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;迭代器遍历的时候,两者性能差不多。 + + +花了两天时间,终于肝完了!相信看完这篇文章后,再有面试官问你 ArrayList 和 LinkedList 有什么区别的话,你一定会胸有成竹地和他扯上半小时了。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/collection/time-complexity.md b/docs/collection/time-complexity.md new file mode 100644 index 0000000000..b9475891f3 --- /dev/null +++ b/docs/collection/time-complexity.md @@ -0,0 +1,113 @@ +--- +title: 衡量Java集合框架执行效率的重要标准:时间复杂度 +shortTitle: 时间复杂度 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java 时间复杂度 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,时间复杂度 +--- + +“二哥,为什么要讲时间复杂度呀?”三妹问。 + +“因为接下来要用到啊。后面我们学习 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 的时候,两者在增删改查时的执行效率就很容易对比清楚了。”我伸了个懒腰后对三妹说,“整体上,集合框架就这么多东西了,随后我们会一一展开来讲,比如说 ArrayList、LinkedList、HashMap 等。”。 + +“好的,二哥。”三妹重新回答沙发上,一盘王者荣耀即将开始。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/treemap.md b/docs/collection/treemap.md new file mode 100644 index 0000000000..8c41e205c2 --- /dev/null +++ b/docs/collection/treemap.md @@ -0,0 +1,256 @@ +--- +title: Java TreeMap详解(附源码分析) +shortTitle: TreeMap详解 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java TreeMap详解 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,TreeMap +--- + + + +TreeMap,虽然也是个 Map,但存在感太低了。我做程序员这十多年里,HashMap 用了超过十年,TreeMap 只用了多字里那么一小会儿一小会儿,真的是,太惨了。 + +虽然 TreeMap 用得少,但还是有用处的。 + +之前 [LinkedHashMap](https://tobebetterjavaer.com/collection/linkedhashmap.html) 那篇文章里提到过了,HashMap 是无序的,所有有了 LinkedHashMap,加上了双向链表后,就可以保持元素的插入顺序和访问顺序,那 TreeMap 呢? + +TreeMap 由红黑树实现,可以保持元素的自然顺序,或者实现了 Comparator 接口的自定义顺序。 + +可能有些同学不知道红黑树,理解起来 TreeMap 就有点难度,那我先来普及一下: + +> 红黑树(英语:Red–black tree)是一种自平衡的二叉查找树(Binary Search Tree),结构复杂,但却有着良好的性能,完成查找、插入和删除的时间复杂度均为 log(n)。 + +二叉查找树又是什么呢? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-c5f29a61-91e0-47e8-9e16-055e1bea5b33.jpg) + +上图中这棵树,就是一颗典型的二叉查找树: + +1)左子树上所有节点的值均小于或等于它的根结点的值。 + +2)右子树上所有节点的值均大于或等于它的根结点的值。 + +3)左、右子树也分别为二叉排序树。 + +理解二叉查找树了吧?不过,二叉查找树有一个不足,就是容易变成瘸子,就是一侧多,一侧少,就像下图这样: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-6c3309a0-3737-4677-99c1-e8a156140d62.jpg) + + +查找的效率就要从 log(n) 变成 o(n) 了,对吧?必须要平衡一下,对吧?于是就有了平衡二叉树,左右两个子树的高度差的绝对值不超过 1,就像下图这样: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-b8f74a3d-baf9-4192-a0e0-cdd07955d784.jpg) + + +红黑树,顾名思义,就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡: + +1)每个节点都只能是红色或者黑色 + +2)根节点是黑色 + +3)每个叶节点(NIL 节点,空节点)是黑色的。 + +4)如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点。 + +5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-3373c98c-d82f-4a7f-949e-a7fe0d99314c.jpg) + + +那,关于红黑树,同学们就先了解到这,脑子里有个大概的印象,知道 TreeMap 是个什么玩意。 + +## 01、自然顺序 + +默认情况下,TreeMap 是根据 key 的自然顺序排列的。比如说整数,就是升序,1、2、3、4、5。 + +``` +TreeMap mapInt = new TreeMap<>(); +mapInt.put(3, "沉默王二"); +mapInt.put(2, "沉默王二"); +mapInt.put(1, "沉默王二"); +mapInt.put(5, "沉默王二"); +mapInt.put(4, "沉默王二"); + +System.out.println(mapInt); +``` + +输出结果如下所示: + +``` +{1=沉默王二, 2=沉默王二, 3=沉默王二, 4=沉默王二, 5=沉默王二} +``` + +TreeMap 是怎么做到的呢?想一探究竟,就得上源码了,来看 TreeMap 的 `put()` 方法(省去了一部分,版本为 JDK 14): + +``` +public V put(K key, V value) { +    TreeMap.Entry t = root; +    int cmp; +    TreeMap.Entry parent; +    // split comparator and comparable paths +    Comparator cpr = comparator; +    if (cpr != null) { +    } +    else { +        @SuppressWarnings("unchecked") +        Comparable k = (Comparable) key; +        do { +            parent = t; +            cmp = k.compareTo(t.key); +            if (cmp < 0) +                t = t.left; +            else if (cmp > 0) +                t = t.right; +            else +                return t.setValue(value); +        } while (t != null); +    } +    return null; +} +``` + +注意 `cmp = k.compareTo(t.key)` 这行代码,就是用来进行 key 的比较的,由于此时 key 是 int,所以就会调用 Integer 类的 `compareTo()` 方法进行比较。 + +``` +public int compareTo(Integer anotherInteger) { +    return compare(this.value, anotherInteger.value); +} + +public static int compare(int x, int y) { +    return (x < y) ? -1 : ((x == y) ? 0 : 1); +} +``` + +那相应的,如果 key 是字符串的话,也就会调用 String 类的 `compareTo()` 方法进行比较。 + +``` +public int compareTo(String anotherString) { +    byte v1[] = value; +    byte v2[] = anotherString.value; +    byte coder = coder(); +    if (coder == anotherString.coder()) { +        return coder == LATIN1 ? StringLatin1.compareTo(v1, v2) +                : StringUTF16.compareTo(v1, v2); +    } +    return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2) +            : StringUTF16.compareToLatin1(v1, v2); +} +``` + +由于内部是由字符串的字节数组的字符进行比较的,是不是听起来很绕?对,就是很绕,所以使用中文字符串作为 key 的话,看不出来效果。 + +``` +TreeMap mapString = new TreeMap<>(); +mapString.put("c", "沉默王二"); +mapString.put("b", "沉默王二"); +mapString.put("a", "沉默王二"); +mapString.put("e", "沉默王二"); +mapString.put("d", "沉默王二"); + +System.out.println(mapString); +``` + +输出结果如下所示: + +``` +{a=沉默王二, b=沉默王二, c=沉默王二, d=沉默王二, e=沉默王二} +``` + +字母的升序,对吧? + +## 02、自定义排序 + +如果自然顺序不满足,那就可以在声明 TreeMap 对象的时候指定排序规则。 + +``` +TreeMap mapIntReverse = new TreeMap<>(Comparator.reverseOrder()); +mapIntReverse.put(3, "沉默王二"); +mapIntReverse.put(2, "沉默王二"); +mapIntReverse.put(1, "沉默王二"); +mapIntReverse.put(5, "沉默王二"); +mapIntReverse.put(4, "沉默王二"); + +System.out.println(mapIntReverse); +``` + +TreeMap 提供了可以指定排序规则的构造方法: + +``` +public TreeMap(Comparator comparator) { +    this.comparator = comparator; +} +``` + +`Comparator.reverseOrder()` 返回的是 ReverseComparator 对象,就是用来反转顺序的,非常方便。 + +所以,输出结果如下所示: + +``` +{5=沉默王二, 4=沉默王二, 3=沉默王二, 2=沉默王二, 1=沉默王二} +``` + +HashMap 是无序的,插入的顺序随着元素的增加会不停地变动。但 TreeMap 能够至始至终按照指定的顺序排列,这对于需要自定义排序的场景,实在是太有用了! + +## 03、排序的好处 + +既然 TreeMap 的元素是经过排序的,那找出最大的那个,最小的那个,或者找出所有大于或者小于某个值的键来说,就方便多了。 + +``` +Integer highestKey = mapInt.lastKey(); +Integer lowestKey = mapInt.firstKey(); +Set keysLessThan3 = mapInt.headMap(3).keySet(); +Set keysGreaterThanEqTo3 = mapInt.tailMap(3).keySet(); + +System.out.println(highestKey); +System.out.println(lowestKey); + +System.out.println(keysLessThan3); +System.out.println(keysGreaterThanEqTo3); +``` + +TreeMap 考虑得很周全,恰好就提供了 `lastKey()`、`firstKey()` 这样获取最后一个 key 和第一个 key 的方法。 + +`headMap()` 获取的是到指定 key 之前的 key;`tailMap()` 获取的是指定 key 之后的 key(包括指定 key)。 + +来看一下输出结果: + +``` +5 +1 +[1, 2] +[3, 4, 5] +``` + +## 04、如何选择 Map + +在学习 TreeMap 之前,我们已经学习了 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 和 [LinkedHashMap](https://tobebetterjavaer.com/collection/linkedhashmap.html) ,那如何从它们三个中间选择呢? + +HashMap、LinkedHashMap、TreeMap 都实现了 Map 接口,并提供了几乎相同的功能(增删改查)。它们之间最大的区别就在于元素的顺序: + +HashMap 完全不保证元素的顺序,添加了新的元素,之前的顺序可能完全逆转。 + +LinkedHashMap 默认会保持元素的插入顺序。 + +TreeMap 默认会保持 key 的自然顺序(根据 `compareTo()` 方法)。 + +来个表格吧,一目了然。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-f94219fb-b6ef-4192-8174-4759498f857f.jpg) + + + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/common-tool/Objects.md b/docs/common-tool/Objects.md new file mode 100644 index 0000000000..908ddc254a --- /dev/null +++ b/docs/common-tool/Objects.md @@ -0,0 +1,86 @@ +--- +title: Objects:专为操作 Java 对象而生的工具类 +shortTitle: Objects对象工具类 +category: + - Java核心 +tag: + - 常用工具类 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Objects:专为操作 Java 对象而生的工具类 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,Objects +--- + +在`jdk7`之后,提供了`Objects`工具类,我们可以通过它操作对象。 + +## 对象判空 +在java中万事万物皆对象,对象的判空可以说无处不在。Objects的`isNull`方法判断对象是否为空,而`nonNull`方法判断对象是否不为空。例如: +```java +Integer integer = new Integer(1); + +if (Objects.isNull(integer)) { + System.out.println("对象为空"); +} + +if (Objects.nonNull(integer)) { + System.out.println("对象不为空"); +} +``` + +## 对象为空抛异常 +如果我们想在对象为空时,抛出空指针异常,可以使用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有坑](https://mp.weixin.qq.com/s?__biz=MzkwNjMwMTgzMQ==&mid=2247493176&idx=1&sn=c445625478a7f8122a6715b64fe6770c&chksm=c0e83ed0f79fb7c6cf2992d24e98f60fd78ca89525b5a3cc07f79dc801dd8e381b1fce03bf5c&token=1124974571&lang=zh_CN#rd)》,里面有非常详细的讲解。 + +## 获取对象的hashCode +如果你想获取某个对象的hashCode,可以使用Objects的`hashCode`方法。例如: +```java +String str = new String("abc"); +System.out.println(Objects.hashCode(str)); +``` +执行结果: +```java +96354 +``` + +Objects的内容先介绍到这里,有兴趣的小伙们,可以看看下面更多的方法: +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/Objects-83489814-9784-4274-841a-27ee75c046ac.jpg) + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ 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..9de3400e8a 100644 --- a/docs/src/common-tool/arrays.md +++ b/docs/common-tool/arrays.md @@ -5,15 +5,15 @@ category: - Java核心 tag: - 常用工具类 -description: 本文详细介绍了Java中的Arrays工具类,阐述了它在数组操作中的实际应用和优势。通过具体的代码示例,展示了如何使用Arrays类处理数组排序、查找、转换等常见问题。学习Arrays工具类的技巧,让您在Java编程中轻松应对各种数组操作,提高开发效率。 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java Arrays工具类的10大常用方法 head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Arrays,数组,java arrays,java 数组 + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,Arrays,数组 --- -“哥,数组专用工具类是专门用来操作[数组](https://javabetter.cn/array/array.html)的吗?比如说创建数组、数组排序、数组检索等等。”三妹的提问其实已经把答案说了出来。 +“哥,数组专用工具类是专门用来操作数组的吗?比如说创建数组、数组排序、数组检索等等。”三妹的提问其实已经把答案说了出来。 “是滴,这里说的数组专用工具类指的是 `java.util.Arrays` 类,基本上常见的数组操作,这个类都提供了静态方法可供直接调用。毕竟数组本身想完成这些操作还是挺麻烦的,有了这层封装,就方便多了。”在回答三妹的同时,我打开 Intellij IDEA,找到了 Arrays 类的源码。 @@ -42,7 +42,7 @@ public class Arrays {} “我们来一个一个学习。” -### 01、创建数组 +## 01、创建数组 使用 Arrays 类创建数组可以通过以下三个方法: @@ -50,7 +50,7 @@ public class Arrays {} - copyOfRange,复制指定范围内的数组到一个新的数组 - fill,对数组进行填充 -#### 1)copyOf +### 1)copyOf 直接来看例子: @@ -80,7 +80,7 @@ private Object[] grow(int minCapacity) { } ``` -#### 2)copyOfRange +### 2)copyOfRange 直接来看例子: @@ -115,7 +115,8 @@ System.out.println(Arrays.toString(abridgementExpanded)); “嗯,我想是 Arrays 的设计者考虑到了数组越界的问题,不然每次调用 Arrays 类就要先判断很多次长度,很麻烦。”稍作思考后,我给出了这样一个回答。 -#### 3)fill + +### 3)fill 直接来看例子: @@ -133,7 +134,7 @@ System.out.println(Arrays.toString(stutter)); 如果想要一个元素完全相同的数组时, `fill()` 方法就派上用场了。 -### 02、比较数组 +## 02、比较数组 Arrays 类的 `equals()` 方法用来判断两个数组是否相等,来看下面这个例子: @@ -176,7 +177,7 @@ public static boolean equals(Object[] a, Object[] a2) { } ``` -因为数组是一个对象,所以先使用“==”操作符进行判断,如果不相等,再判断是否为 null,其中一个为 null,返回 false;紧接着判断 length,不等的话,返回 false;否则的话,依次调用 `Objects.equals()` 比较相同位置上的元素是否相等。 +因为数组是一个对象,所以先使用“==”操作符进行判断,如果不相等,再判断是否为 null,两个都为 null,返回 false;紧接着判断 length,不等的话,返回 false;否则的话,依次调用 `Objects.equals()` 比较相同位置上的元素是否相等。 “这段代码还是非常严谨的,对吧?三妹,这也就是我们学习源码的意义,欣赏的同时,可以学习源码作者清晰的编码思路。”我语重心长地给三妹讲。 @@ -214,7 +215,7 @@ System.out.println(Arrays.hashCode(new String[] { "沉", "默", "王", "二" })) 两个数组的哈希值相等,毕竟元素是一样的。但这样确实不够严谨,优先使用 `Objects.equals()` 方法,当我们想快速确认两个数组是否相等时,可以通过比较 hashCode 来确认——算是投机取巧吧,高收益高风险,哈哈。 -### 03、数组排序 +## 03、数组排序 Arrays 类的 `sort()` 方法用来对数组进行排序,来看下面这个例子: @@ -237,7 +238,7 @@ System.out.println(Arrays.toString(sorted)); “不要紧的,后面学了数据结构与算法后,就明白了,现在了解这个东西即可。”我赶紧甩出了安抚大法。 -### 04、数组检索 +## 04、数组检索 数组排序后就可以使用 Arrays 类的 `binarySearch()` 方法进行二分查找了。否则的话,只能线性检索,效率就会低很多。 @@ -264,11 +265,11 @@ System.out.println(caseInsensitive); 三妹若有所思的点了点头。 -### 05、数组转流 +## 05、数组转流 “流是什么呀?”三妹好奇的问。 -“流的英文单词是 Stream,它可以极大提高 Java 程序员的生产力,让程序员写出高效、干净、简洁的代码。 这种风格将要处理的集合看作是一种流,想象一下水流在管道中流过的样子,我们可以在管道中对流进行处理,比如筛选、排序等等。[Stream 具体怎么使用](https://javabetter.cn/java8/stream.html),我们留到后面再详细地讲,这里你先有一个大致的印象就可以了。”我回答到。 +“流的英文单词是 Stream,它可以极大提高 Java 程序员的生产力,让程序员写出高效、干净、简洁的代码。 这种风格将要处理的集合看作是一种流,想象一下水流在管道中流过的样子,我们可以在管道中对流进行处理,比如筛选、排序等等。Stream 具体怎么使用,我们留到后面再详细地讲,这里你先有一个大致的印象就可以了。”我回答到。 Arrays 类的 `stream()` 方法可以将数组转换成流: @@ -291,7 +292,7 @@ Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: origin(2) > at java.base/java.util.Spliterators.checkFromToBounds(Spliterators.java:387) ``` -### 06、打印数组 +## 06、打印数组 因为数组是一个对象,直接 `System.out.println` 的话,结果是这样的: @@ -299,7 +300,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) { @@ -335,9 +336,9 @@ public static String toString(Object[] a) { “哦----------”三妹似乎明白了什么。 -### 07、数组转 List +## 07、数组转 List -尽管数组非常强大,但它自身可以操作的工具方法很少,比如说判断数组中是否包含某个值。如果能转成 List 的话,就简便多了,因为 Java 的[集合框架 List](https://javabetter.cn/collection/gailan.html) 中封装了很多常用的方法。 +尽管数组非常强大,但它自身可以操作的工具方法很少,比如说判断数组中是否包含某个值。如果能转成 List 的话,就简便多了,因为 Java 的集合框架 List 中封装了很多常用的方法。 ```java String[] intro = new String[] { "沉", "默", "王", "二" }; @@ -345,7 +346,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("三"); @@ -368,9 +369,9 @@ rets1.add("三"); rets1.remove("二"); ``` -### 08、setAll +## 08、setAll -Java 8 新增了 `setAll()` 方法,它提供了一个[函数式编程](https://javabetter.cn/java8/Lambda.html)的入口,可以对数组的元素进行填充: +Java 8 新增了 `setAll()` 方法,它提供了一个函数式编程的入口,可以对数组的元素进行填充: ```java int[] array = new int[10]; @@ -388,7 +389,7 @@ i 就相当于是数组的下标,值从 0 开始,到 9 结束,那么 `i * 可以用来为新数组填充基于原来数组的新元素。 -### 09、parallelPrefix +## 09、parallelPrefix `parallelPrefix()` 方法和 `setAll()` 方法一样,也是 Java 8 之后提供的,提供了一个函数式编程的入口,通过遍历数组中的元素,将当前下标位置上的元素与它之前下标的元素进行操作,然后将操作后的结果覆盖当前下标位置上的元素。 @@ -425,7 +426,7 @@ System.out.println(Arrays.toString(arr)); - 第三次是 6 和 4 相加,结果是 10,也就是第二次的结果和下标为 3 的元素相加的结果 -### 10、总结 +## 10、总结 “好了,三妹,就先学到这吧。如果你以后翻 Java 源码的时候,只要是用到数组的,尤其是 ArrayList 类,就可以看到 Arrays 类的很多影子。” @@ -433,11 +434,11 @@ 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/) +---- +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ 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..1920081871 --- /dev/null +++ b/docs/common-tool/collections.md @@ -0,0 +1,267 @@ +--- +title: Java Collections:专为集合框架而生的工具类 +shortTitle: Collections工具类 +category: + - Java核心 +tag: + - 常用工具类 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java Collections:专为集合框架而生的工具类 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,Collections,集合框架 +--- + + +Collections 是 JDK 提供的一个工具类,位于 java.util 包下,提供了一系列的静态方法,方便我们对集合进行各种骚操作,算是集合框架的一个大管家。 + +还记得我们前面讲过的 [Arrays 工具类](https://mp.weixin.qq.com/s/9dYmKXEErZbyPJ_GxwWYug)吗?可以回去温习下。 + +Collections 的用法很简单,在 Intellij IDEA 中敲完 `Collections.` 之后就可以看到它提供的方法了,大致看一下方法名和参数就能知道这个方法是干嘛的。 + + +![](http://cdn.tobebetterjavaer.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://mp.weixin.qq.com/s/qk_neCdzM3aB6pVWVTHhNw)的,这个我们前面讲到了。那其实 ArrayList 也是线程不安全的,没法在多线程环境下使用,那 Collections 工具类中提供了多个 synchronizedXxx 方法,这些方法会返回一个同步的对象,从而解决多线程中访问集合时的安全问题。 + +![](http://cdn.tobebetterjavaer.com/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 这样一个工具类吗? + +这才是高手要思考的一个问题。 + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + + diff --git a/docs/src/common-tool/guava.md b/docs/common-tool/guava.md similarity index 79% rename from docs/src/common-tool/guava.md rename to docs/common-tool/guava.md index b77fd0c43b..c61a576bdb 100644 --- a/docs/src/common-tool/guava.md +++ b/docs/common-tool/guava.md @@ -1,31 +1,37 @@ --- -title: Guava:Google开源的Java工具库,太强大了 -shortTitle: Guava工具库 +title: Guava:Google开源的工具库,太强大了 +shortTitle: Guava工具类 category: - Java核心 tag: - 常用工具类 -description: 描述:本文详细介绍了Google开源的Java工具库Guava,阐述了它在简化Java编程中的实际应用和优势。通过具体的代码示例,展示了如何使用Guava解决字符串处理、集合操作、缓存等常见问题。学习Guava的技巧,让您在Java编程中更加轻松、高效,享受编程的乐趣。 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Guava:Google开源的工具库,太强大了 head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Guava,java guava,google guava + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,Guava --- -### 01、前世今生 + +## 01、前世今生 你好呀,我是 Guava。 我由 Google 公司开源,目前在 GitHub 上已经有 39.9k 的铁粉了,由此可以证明我的受欢迎程度。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/guava-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/guava-01.png) + 我的身体里主要包含有这些常用的模块:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等。新版的 JDK 中已经直接把我引入了,可想而知我有多优秀,忍不住骄傲了。 这么说吧,学好如何使用我,能让你在编程中变得更快乐,写出更优雅的代码! -### 02、引入 Guava +*PS:star 这种事,只能求,不求没效果😭😭😭。二哥开源的《Java 程序员进阶之路》专栏在 GitHub 上已经收获了 595 枚星标,铁粉们赶紧去点点啦,帮二哥冲 600 star,笔芯*! + +>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +## 02、引入 Guava 如果你要在 Maven 项目使用我的话,需要先在 pom.xml 文件中引入我的依赖。 @@ -39,11 +45,11 @@ head: 一点要求,JDK 版本需要在 8 以上。 -### 03、基本工具 +## 03、基本工具 -Doug Lea,java.util.concurrent 包的作者,曾说过一句话:“[null 真糟糕](https://javabetter.cn/exception/npe.html)”。Tony Hoare,图灵奖得主、快速排序算法的作者,当然也是 null 的创建者,也曾说过类似的话:“null 的使用,让我损失了十亿美元。”鉴于此,我用 Optional 来表示可能为 null 的对象。 +Doug Lea,java.util.concurrent 包的作者,曾说过一句话:“null 真糟糕”。Tony Hoare,图灵奖得主、快速排序算法的作者,当然也是 null 的创建者,也曾说过类似的话:“null 的使用,让我损失了十亿美元。”鉴于此,我用 Optional 来表示可能为 null 的对象。 -![](https://cdn.paicoding.com/stutymore/guava-20230329172935.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/guava-02.png) 代码示例如下所示。 @@ -54,10 +60,12 @@ possible.isPresent(); // returns true possible.get(); // returns 5 ``` -我大哥 Java 在 JDK 8 中新增了 [Optional 类](https://javabetter.cn/java8/optional.html),显然是从我这借鉴过去的,不过他的和我的有些不同。 +我大哥 Java 在 JDK 8 中新增了 [Optional 类](https://mp.weixin.qq.com/s/PqK0KNVHyoEtZDtp5odocA),显然是从我这借鉴过去的,不过他的和我的有些不同。 - 我的 Optional 是 abstract 的,意味着我可以有子类对象;我大哥的是 final 的,意味着没有子类对象。 + - 我的 Optional 实现了 Serializable 接口,可以序列化;我大哥的没有。 + - 我的一些方法和我大哥的也不尽相同。 使用 Optional 除了赋予 null 语义,增加了可读性,最大的优点在于它是一种傻瓜式的防护。Optional 迫使你积极思考引用缺失的情况,因为你必须显式地从 Optional 获取引用。 @@ -68,17 +76,19 @@ possible.get(); // returns 5 - 常见的 Object 方法,比如说 Objects.equals、Objects.hashCode,JDK 7 引入的 Objects 类提供同样的方法,当然也是从我这借鉴的灵感。 - 更强大的比较器 -### 04、集合 +## 04、集合 首先我来说一下,为什么需要不可变集合。 - 保证线程安全。在并发程序中,使用不可变集合既保证线程的安全性,也大大地增强了并发时的效率(跟并发锁方式相比)。 + - 如果一个对象不需要支持修改操作,不可变的集合将会节省空间和时间的开销。 + - 可以当作一个常量来对待,并且集合中的对象在以后也不会被改变。 与 JDK 中提供的不可变集合相比,我提供的 Immutable 才是真正的不可变,我为什么这么说呢?来看下面这个示例。 -下面的代码利用 JDK 的 [`Collections.unmodifiableList(list)`](https://javabetter.cn/common-tool/collections.html) 得到一个不可修改的集合 unmodifiableList。 +下面的代码利用 JDK 的 `Collections.unmodifiableList(list)` 得到一个不可修改的集合 unmodifiableList。 ```java List list = new ArrayList(); @@ -125,18 +135,18 @@ 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(); -} + /** + * 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 并不会因此而发生改变。 @@ -144,11 +154,15 @@ public final boolean add(E e) { 除了不可变集合以外,我还提供了新的集合类型,比如说: - Multiset,可以多次添加相等的元素。当把 Multiset 看成普通的 Collection 时,它表现得就像无序的 ArrayList;当把 Multiset 看作 `Map` 时,它也提供了符合性能期望的查询操作。 + - Multimap,可以很容易地把一个键映射到多个值。 + - BiMap,一种特殊的 Map,可以用 `inverse()` 反转 `BiMap` 的键值映射;保证值是唯一的,因此 `values()` 返回 Set 而不是普通的 Collection。 -### 05、字符串处理 + + +## 05、字符串处理 字符串表示字符的不可变序列,创建后就不能更改。在我们日常的工作中,字符串的使用非常频繁,熟练的对其操作可以极大的提升我们的工作效率。 @@ -168,7 +182,7 @@ Splitter.on(',') .split("雷军,乔布斯,, 沉默王二"); ``` -### 06、缓存 +## 06、缓存 缓存在很多场景下都是相当有用的。你应该知道,检索一个值的代价很高,尤其是需要不止一次获取值的时候,就应当考虑使用缓存。 @@ -236,18 +250,20 @@ MyRemovalListener 作为缓存元素失效时的监听类,在有元素缓存 LoadingCache 就是缓存的主要操作对象了,常用的就是其中的 put 和 get 方法了。 -### 07、集合工具 + +## 07、集合工具 `com.google.common.collect`包下的集合工具:`Lists`也非常强大。 -#### 创建空集合 + +### 创建空集合 有时候,我们想创建一个空集合。这时可以用Lists的`newArrayList`方法,例如: ```java List list = Lists.newArrayList(); ``` -#### 快速初始化集合 +### 快速初始化集合 有时候,我们想给一个集合中初始化一些元素。这时可以用Lists的newArrayList方法,例如: ```java @@ -258,7 +274,7 @@ List list = Lists.newArrayList(1, 2, 3); [1, 2, 3] ``` -#### 笛卡尔积 +### 笛卡尔积 如果你想将两个集合做`笛卡尔积`,Lists的`cartesianProduct`方法可以帮你实现: ```java @@ -272,75 +288,63 @@ System.out.println(productList); [[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`方法。例如: - +### 流处理 +如果我们想把某个集合转换成另外一个接口,可以使用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还有其他的好用的工具,我在这里只是抛砖引玉,有兴趣的小伙伴,可以仔细研究一下。 +Lists还有其他的好用的工具,我在这里只是抛砖引玉,有兴趣的朋友,可以仔细研究一下。 +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/guava-4b962b06-a626-4707-9fe9-f5729536d9c5.jpg) -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/guava-4b962b06-a626-4707-9fe9-f5729536d9c5.jpg) - -### 08、尾声 +## 07、尾声 上面介绍了我认为最常用的功能,作为 Google 公司开源的 Java 开发核心库,个人觉得实用性还是很高的(不然呢?嘿嘿嘿)。引入到你的项目后不仅能快速的实现一些开发中常用的功能,而且还可以让代码更加的优雅简洁。 我觉得适用于每一个 Java 项目,至于其他的一些功能,比如说散列、事件总线、数学运算、反射,就等待你去发掘了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/guava-03.png) +![](http://cdn.tobebetterjavaer.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/) +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/common-tool/hutool.md b/docs/common-tool/hutool.md similarity index 83% rename from docs/src/common-tool/hutool.md rename to docs/common-tool/hutool.md index d231603426..f56c3d55bd 100644 --- a/docs/src/common-tool/hutool.md +++ b/docs/common-tool/hutool.md @@ -1,29 +1,29 @@ --- title: Hutool:国产良心工具包,让你的Java变得更甜 -shortTitle: Hutool工具类库 +shortTitle: Hutool工具类 category: - Java核心 tag: - 常用工具类 -description: 本文详细介绍了国产Java工具包Hutool,阐述了它在简化Java编程中的实际应用和优势。通过具体的代码示例,展示了如何使用Hutool解决字符串处理、集合操作、日期时间处理等常见问题。学习Hutool的技巧,让您在Java编程中更加轻松、高效,享受编程的乐趣。 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Hutool:国产良心工具包,让你的Java变得更甜 head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Hutool,java hutool + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,Hutool --- - 读者群里有个小伙伴感慨说,“Hutool 这款开源类库太厉害了,基本上该有该的工具类,它里面都有。”讲真的,我平常工作中也经常用 Hutool,它确实可以帮助我们简化每一行代码,使 Java 拥有函数式语言般的优雅,让 Java 语言变得“甜甜的”。 -Hutool 的作者在[官网](https://hutool.cn/)上说,Hutool 是 Hu+tool 的自造词(好像不用说,我们也能猜得到),“Hu”用来致敬他的“前任”公司,“tool”就是工具的意思,谐音就有意思了,“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”(一个开源类库,上升到了哲学的高度,作者厉害了)。 +Hutool 的作者在官网上说,Hutool 是 Hu+tool 的自造词(好像不用说,我们也能猜得到),“Hu”用来致敬他的“前任”公司,“tool”就是工具的意思,谐音就有意思了,“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”(一个开源类库,上升到了哲学的高度,作者厉害了)。 看了一下开发团队的一个成员介绍,一个 Java 后端工具的作者竟然爱前端、爱数码,爱美女,嗯嗯嗯,确实“难得糊涂”(手动狗头)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-01.png) + 废话就说到这,来吧,实操走起! -### 01、引入 Hutool +## 01、引入 Hutool Maven 项目只需要在 pom.xml 文件中添加以下依赖即可。 @@ -40,15 +40,15 @@ 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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-02.png) 非常多,非常全面,鉴于此,我只挑选一些我喜欢的来介绍下(偷偷地告诉你,我就是想偷懒)。 -### 02、类型转换 +## 02、类型转换 类型转换在 Java 开发中很常见,尤其是从 HttpRequest 中获取参数的时候,前端传递的是整型,但后端只能先获取到字符串,然后再调用 `parseXXX()` 方法进行转换,还要加上判空,很繁琐。 @@ -74,7 +74,7 @@ String unicodeStr = "沉默王二"; String unicode = Convert.strToUnicode(unicodeStr); ``` -### 03、日期时间 +## 03、日期时间 JDK 自带的 Date 和 Calendar 不太好用,Hutool 封装的 DateUtil 用起来就舒服多了! @@ -135,9 +135,9 @@ String zodiac = DateUtil.getZodiac(Month.DECEMBER.getValue(), 10); String chineseZodiac = DateUtil.getChineseZodiac(1989); ``` -### 04、IO 流相关 +## 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 +160,11 @@ long copySize = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE); 在实际编码当中,我们通常需要从某些文件里面读取一些数据,比如配置文件、文本文件、图片等等,那这些文件通常放在什么位置呢? -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-03.png) 放在项目结构图中的 resources 目录下,当项目编译后,会出现在 classes 目录下。对应磁盘上的目录如下图所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-04.png) 当我们要读取文件的时候,我是不建议使用绝对路径的,因为操作系统不一样的话,文件的路径标识符也是不一样的。最好使用相对路径。 @@ -180,7 +180,7 @@ FileUtil.getInputStream("origin.txt") FileUtil.getInputStream("hutool/origin.txt") ``` -### 05、字符串工具 +## 05、字符串工具 Hutool 封装的字符串工具类 StrUtil 和 Apache Commons Lang 包中的 StringUtils 类似,作者认为优势在于 Str 比 String 短,尽管我不觉得。不过,我倒是挺喜欢其中的一个方法的: @@ -190,7 +190,7 @@ String str = StrUtil.format(template, "沉默王二", "沉默王二"); // 沉默王二,一枚沉默但有趣的程序员,喜欢他的文章的话,请微信搜索沉默王二 ``` -### 06、反射工具 +## 06、反射工具 反射机制可以让 Java 变得更加灵活,因此在某些情况下,反射可以做到事半功倍的效果。Hutool 封装的反射工具 ReflectUtil 包括: @@ -253,7 +253,7 @@ public class ReflectDemo { } ``` -### 07、压缩工具 +## 07、压缩工具 在 Java 中,对文件、文件夹打包压缩是一件很繁琐的事情,Hutool 封装的 ZipUtil 针对 java.util.zip 包做了优化,可以使用一个方法搞定压缩和解压,并且自动处理文件和目录的问题,不再需要用户判断,大大简化的压缩解压的复杂度。 @@ -262,7 +262,7 @@ ZipUtil.zip("hutool", "hutool.zip"); File unzip = ZipUtil.unzip("hutool.zip", "hutoolzip"); ``` -### 08、身份证工具 +## 08、身份证工具 Hutool 封装的 IdcardUtil 可以用来对身份证进行验证,支持大陆 15 位、18 位身份证,港澳台 10 位身份证。 @@ -274,7 +274,7 @@ boolean valid = IdcardUtil.isValidCard(ID_18); boolean valid15 = IdcardUtil.isValidCard(ID_15); ``` -### 09、扩展 HashMap +## 09、扩展 HashMap Java 中的 HashMap 是强类型的,而 Hutool 封装的 Dict 对键的类型要求没那么严格。 @@ -288,7 +288,7 @@ int age = dict.getInt("age"); String name = dict.getStr("name"); ``` -### 10、控制台打印 +## 10、控制台打印 本地编码的过程中,经常需要使用 `System.out` 打印结果,但是往往一些复杂的对象不支持直接打印,比如说数组,需要调用 `Arrays.toString`。Hutool 封装的 Console 类借鉴了 JavaScript 中的 `console.log()`,使得打印变成了一个非常便捷的方式。 @@ -308,7 +308,7 @@ public class ConsoleDemo { } ``` -### 11、字段验证器 +## 11、字段验证器 做 Web 开发的时候,后端通常需要对表单提交过来的数据进行验证。Hutool 封装的 Validator 可以进行很多有效的条件验证: @@ -317,16 +317,16 @@ public class ConsoleDemo { - 是不是电话号码 - 等等 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-05.png) ```java Validator.isEmail("沉默王二"); Validator.isMobile("itwanger.com"); ``` -### 12、双向查找 Map +## 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,9 +344,9 @@ biMap.getKey("沉默王三"); 在实际的开发工作中,其实我更倾向于使用 Guava 的 BiMap,而不是 Hutool 的。这里提一下,主要是我发现了 Hutool 在线文档上的一处错误,提了个 issue(从中可以看出我一颗一丝不苟的心和一双清澈明亮的大眼睛啊)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-06.png) -### 13、图片工具 +## 13、图片工具 Hutool 封装的 ImgUtil 可以对图片进行缩放、裁剪、转为黑白、加水印等操作。 @@ -386,9 +386,9 @@ ImgUtil.pressText(// 趁机让大家欣赏一下二哥帅气的真容。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-07.png) -### 14、配置文件 +## 14、配置文件 >众所周知,Java 中广泛应用的配置文件 Properties 存在一个特别大的诟病:不支持中文。每次使用时,如果想存放中文字符,就必须借助 IDE 相关插件才能转为 Unicode 符号,而这种反人类的符号在命令行下根本没法看。 @@ -427,7 +427,7 @@ public class SettingDemo { } ``` -### 15、日志工厂 +## 15、日志工厂 Hutool 封装的日志工厂 LogFactory 兼容了各大日志框架,使用起来也非常简便。 @@ -449,7 +449,7 @@ public class LogDemo { StaticLog.info("爽啊 {}.", "沉默王二的文章"); ``` -### 16、缓存工具 +## 16、缓存工具 CacheUtil 是 Hutool 封装的创建缓存的快捷工具类,可以创建不同的缓存对象: @@ -500,7 +500,7 @@ String value2 = lruCache.get("key2"); System.out.println(value2); ``` -### 17、加密解密 +## 17、加密解密 加密分为三种: @@ -548,7 +548,7 @@ public class SecureUtilDemo { } ``` -### 18、其他类库 +## 18、其他类库 Hutool 中的类库还有很多,尤其是一些对第三方类库的进一步封装,比如邮件工具 MailUtil,二维码工具 QrCodeUtil,Emoji 工具 EmojiUtil,小伙伴们可以参考 Hutool 的官方文档:https://www.hutool.cn/ @@ -556,11 +556,10 @@ 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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/common-tool/utils.md b/docs/common-tool/utils.md new file mode 100644 index 0000000000..a15f76432d --- /dev/null +++ b/docs/common-tool/utils.md @@ -0,0 +1,528 @@ +--- +title: 这10个工具类,让我的开发效率提升了50% +shortTitle: 10个提升开发效率的Java工具类 +category: + - Java核心 +tag: + - 常用工具类 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,这10个工具类,让我的开发效率提升了50% +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,工具类,轮子 +--- + +## 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 + +本机执行后截图如下: + +![本机](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/IpUtil-f35dc96f-b8ac-43d3-9393-0ff565e85fb9.jpg) + + +阿里云机器执行后截图如下: + +![阿里云](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/IpUtil-f50b0de2-cf0d-4e9b-8f10-838ea4b47fd8.jpg) + + +再问一句,那是否就真的没有问题了呢? + +- 在某些情况下,可能返回的是 `127.0.0.1` + + +在虚拟机中执行时,就可能遇到这个问题,截图如下 + + +![虚拟机](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/IpUtil-7c14024b-57d1-4086-9f51-d7bf312b5fbf.jpg) + +### 2. 进阶版 + +做一点简单的改动,获取IpV4的地址,源码如下 + +```java +/** + * 直接根据第一个网卡地址作为其内网ipv4地址,避免返回 127.0.0.1 + * + * @return + */ +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; + } + 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); + } +} +``` + +再次测试,输出如下 + + +![虚拟机](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/IpUtil-cd2f2acb-a6ea-4675-82a8-95a7e05c8498.jpg) + + +### 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 + */ + 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; + } + 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); + } + } + + public static String getLocalIP() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } +} +``` + +## CollectionUtils:Spring和Apache都有提供的集合工具类 + +对集合操作,除了前面说的`Collections`工具类之后,`CollectionUtils`工具类也非常常用。 + +目前比较主流的是`spring`的`org.springframework.util`包下的CollectionUtils工具类。 +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/CollectionUtils-3433117c-4ab2-4ac4-bf5b-4b729d87fc9a.jpg) + +和`apache`的`org.apache.commons.collections`包下的CollectionUtils工具类。 +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/CollectionUtils-1bc7dfe9-f459-47bb-ae4b-2a25d4be96c1.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/CollectionUtils-2b8630a3-141b-4f18-9f54-5a37fc818420.jpg) +> 我个人更推荐使用apache的包下的CollectionUtils工具类,因为它的工具更多更全面。 + +举个简单的例子,`spring`的CollectionUtils工具类没有判断集合不为空的方法。而`apache`的CollectionUtils工具类却有。 + +下面我们以`apache`的CollectionUtils工具类为例,介绍一下常用方法。 + +### 集合判空 +通过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] +``` +说句实话,对两个集合的操作,在实际工作中用得挺多的,特别是很多批量的场景中。以前我们需要写一堆代码,但没想到有现成的轮子。 + +## StringUtils:专为 Java 字符串而生的工具类 + +`字符串`(String)在我们的日常工作中,用得非常非常非常多。 + +在我们的代码中经常需要对字符串判空,截取字符串、转换大小写、分隔字符串、比较字符串、去掉多余空格、拼接字符串、使用正则表达式等等。 + +如果只用String类提供的那些方法,我们需要手写大量的额外代码,不然容易出现各种异常。 + +现在有个好消息是:`org.apache.commons.lang3`包下的`StringUtils`工具类,给我们提供了非常丰富的选择。 + +### 字符串判空 +其实空字符串,不只是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 +``` +当然还有很多实用的方法,我在这里就不一一介绍了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/utils-68f94af9-d2ea-46c2-81b4-7d7e08891550.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/utils-7314260e-4e85-4110-a50d-3bedcbbeb616.jpg) + +## MDC:一个线程安全的参数传递工具类 + +`MDC`是`org.slf4j`包下的一个类,它的全称是Mapped Diagnostic Context,我们可以认为它是一个线程安全的存放诊断日志的容器。 + +MDC的底层是用了`ThreadLocal`来保存数据的。 + +我们可以用它传递参数。 + +例如现在有这样一种场景:我们使用`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还有很多有用的方法,等待着你去发掘。感兴趣的朋友,可以看看下面内容: +![](http://cdn.tobebetterjavaer.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比较感兴趣,可以看看下面内容: +![](http://cdn.tobebetterjavaer.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)); +``` + +当然这个类还有不少有趣的方法,感兴趣的朋友,可以看看下面内容: +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/utils-0a4ecb9c-b9d2-4090-a7b7-c626a0672b94.jpg) + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/cs/os.md b/docs/cs/os.md new file mode 100644 index 0000000000..6751c6bef1 --- /dev/null +++ b/docs/cs/os.md @@ -0,0 +1,1342 @@ +--- +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) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-5ad16ae7-059f-44f1-8236-697b203bb72e.png) + +## 计算机结构 + +现代计算机模型是基于-**冯诺依曼计算机模型** + +计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进行指定的运算和逻辑操作等加工,然后再按地址把结果送到内存中去,接下来,再取出第二条指令,在控制器的指挥下完成规定操作,依此进行下去。直至遇到停止指令 + +程序与数据一样存贮,按程序编排的顺序,一步一步地取出指令,自动地完成指令规定的操作是计算机最基本的工作模型 + +**计算机五大核心组成部分** + +控制器:是整个计算机的中枢神经,其功能是对程序规定的控制信息进行解释,根据其要求进行控制,调度程序、数据、地址,协调计算机各部分工作及内存与外设的访问等。 + +运算器:运算器的功能是对数据进行各种算术运算和逻辑运算,即对数据进行加工处理。 + +存储器:存储器的功能是存储程序、数据和各种信号、命令等信息,并在需要时提供这些信息。 + +输入:输入设备是计算机的重要组成部分,输入设备与输出设备合你为外部设备,简称外设,输入设备的作用是将程序、原始数据、文字、字符、控制命令或现场采集的数据等信息输入到计算机。 + +> 常见的输入设备有键盘、鼠标器、光电输入机、磁带机、磁盘机、光盘机等。 + +输出:输出设备与输入设备同样是计算机的重要组成部分,它把外算机的中间结果或最后结果、机内的各种数据符号及文字或各种控制信号等信息输出出来,微机常用的输出设备有显示终端CRT、打印机、激光印字机、绘图仪及磁带、光盘机等。 + +**计算机结构分成以下 5 个部分:** + +输入设备;输出设备;内存;中央处理器;总线。 + +![](http://cdn.tobebetterjavaer.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。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-214a4294-df48-4ed4-8f6a-15c93c02037d.png) + +当 CPU 需要内存中某个数据的时候,如果寄存器中有这个数据,我们可以直接使用;如果寄存器中没有这个数据,我们就要先查询 L1 缓存;L1 中没有,再查询 L2 缓存;L2 中没有再查询 L3 缓存;L3 中没有,再去内存中拿。 + +![](http://cdn.tobebetterjavaer.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 的指令周期**。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-0e135ca2-26fa-4495-8a4b-3c9c05a4341e.png) + +### 总线 + +CPU 和内存以及其他设备之间,也需要通信,因此我们用一种特殊的设备进行控制,就是总线。 + +- 地址总线,用于指定 CPU 将要操作的内存地址; +- 数据总线,用于读写内存的数据; +- 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线; + +当 CPU 要读写内存数据的时候,一般需要通过两个总线: + +- 首先要通过地址总线来指定内存的地址; +- 再通过数据总线来传输数据; + +### 输入、输出设备 + +输入设备向计算机输入数据,计算机经过计算,将结果通过输出设备向外界传达。 + +如果输入设备、输出设备想要和 CPU 进行交互,比如说用户按键需要 CPU 响应,这时候就需要用到控制总线。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 基础知识 + +### 中断 + +**中断的类型** + +* 按照中断的触发方分成同步中断和异步中断; + +* 根据中断是否强制触发分成可屏蔽中断和不可屏蔽中断。 + +中断可以由 CPU 指令直接触发,这种主动触发的中断,叫作同步中断。 + +> 同步中断有几种情况。 + +* 比如系统调用,需要从用户态切换内核态,这种情况需要程序触发一个中断,叫作陷阱(Trap),中断触发后需要继续执行系统调用。 + +* 还有一种同步中断情况是错误(Fault),通常是因为检测到某种错误,需要触发一个中断,中断响应结束后,会重新执行触发错误的地方,比如后面我们要学习的缺页中断。 + +* 最后还有一种情况是程序的异常,这种情况和 Trap 类似,用于实现程序抛出的异常。 + +另一部分中断不是由 CPU 直接触发,是因为需要响应外部的通知,比如响应键盘、鼠标等设备而触发的中断,这种中断我们称为异步中断。 + +CPU 通常都支持设置一个中断屏蔽位(一个寄存器),设置为 1 之后 CPU 暂时就不再响应中断。 + +对于键盘鼠标输入,比如陷阱、错误、异常等情况,会被临时屏蔽。 + +但是对于一些特别重要的中断,比如 CPU 故障导致的掉电中断,还是会正常触发。 + +**可以被屏蔽的中断我们称为可屏蔽中断,多数中断都是可屏蔽中断。** + +### 内核态和用户态 + +**什么是用户态和内核态** + +Kernel 运行在超级权限模式下,所以拥有很高的权限。 + +按照权限管理的原则,多数应用程序应该运行在最小权限下。 + +因此,很多操作系统,将内存分成了两个区域: + +* 内核空间(Kernal Space),这个空间只有内核程序可以访问; + +* 用户空间(User Space),这部分内存专门给应用程序使用。 + +用户空间中的代码被限制了只能使用一个局部的内存空间,我们说这些程序在用户态 执行。 + +内核空间中的代码可以访问所有内存,我们称这些程序在内核态 执行。 + +> 按照级别分: + +当程序运行在0级特权级上时,就可以称之为运行在内核态 + +当程序运行在3级特权级上时,就可以称之为运行在用户态 + +运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。 + +当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件) + +**这两种状态的主要差别** + +处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的 + +处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。 + +**为什么要有用户态和内核态** + +由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据,并发送到网络 + +**用户态与内核态的切换** + +所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据,或者从键盘获取输入等,而唯一可以做这些事情的就是操作系统,所以此时程序就需要先操作系统请求以程序的名义来执行这些操作 + +**用户态和内核态的转换** + +> 系统调用 + +用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如fork()实际上就是执行了一个创建新进程的系统调用 + +而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断 + +**举例:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-29b6f34f-a8b3-48ec-8e5c-aea1036ea16a.png) + +如上图所示:内核程序执行在内核态(Kernal Mode),用户程序执行在用户态(User Mode)。 + +当发生系统调用时,用户态的程序发起系统调用,因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。 + +发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序,内核程序开始执行,也就是开始处理系统调用。 + +内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。 + +> 异常 + +当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常 + +> 外围设备的中断 + +当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换 + +比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 线程 + +线程:系统分配处理器时间资源的基本单元,是程序执行的最小单位 + +线程可以看做轻量级的进程,共享内存空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。 + +在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行) + +进程可以通过 API 创建用户态的线程,也可以通过系统调用创建内核态的线程。 + +### 用户态线程 + +用户态线程也称作用户级线程,操作系统内核并不知道它的存在,它完全是在用户空间中创建。 + +用户级线程有很多优势,比如: + +* 管理开销小:创建、销毁不需要系统调用。 + +* 切换成本低:用户空间程序可以自己维护,不需要走操作系统调度。 + +但是这种线程也有很多的缺点: + +* 与内核协作成本高:比如这种线程完全是用户空间程序在管理,当它进行 I/O 的时候,无法利用到内核的优势,需要频繁进行用户态到内核态的切换。 + +* 线程间协作成本高:设想两个线程需要通信,通信需要 I/O,I/O 需要系统调用,因此用户态线程需要额外的系统调用成本。 + +* 无法利用多核优势:比如操作系统调度的仍然是这个线程所属的进程,所以无论每次一个进程有多少用户态的线程,都只能并发执行一个线程,因此一个进程的多个线程无法利用多核的优势。 + +操作系统无法针对线程调度进行优化:当一个进程的一个用户态线程阻塞(Block)了,操作系统无法及时发现和处理阻塞问题,它不会更换执行其他线程,从而造成资源浪费。 + +### 内核态线程 + +内核态线程也称作内核级线程(Kernel Level Thread),这种线程执行在内核态,可以通过系统调用创造一个内核级线程。 + +内核级线程有很多优势: + +* 可以利用多核 CPU 优势:内核拥有较高权限,因此可以在多个 CPU 核心上执行内核线程。 + +* 操作系统级优化:内核中的线程操作 I/O 不需要进行系统调用;一个内核线程阻塞了,可以立即让另一个执行。 + +当然内核线程也有一些缺点: + +* 创建成本高:创建的时候需要系统调用,也就是切换到内核态。 + +* 扩展性差:由一个内核程序管理,不可能数量太多。 + +* 切换成本较高:切换的时候,也同样存在需要内核操作,需要切换内核态。 + +**用户态线程和内核态线程之间的映射关系** + +如果有一个用户态的进程,它下面有多个线程,如果这个进程想要执行下面的某一个线程,应该如何做呢? + +> 这时,比较常见的一种方式,就是将需要执行的程序,让一个内核线程去执行。 + +毕竟,内核线程是真正的线程,因为它会分配到 CPU 的执行资源。 + +如果一个进程所有的线程都要自己调度,相当于在进程的主线程中实现分时算法调度每一个线程,也就是所有线程都用操作系统分配给主线程的时间片段执行。 + +> 这种做法,相当于操作系统调度进程的主线程;进程的主线程进行二级调度,调度自己内部的线程。 + +这样操作劣势非常明显,比如无法利用多核优势,每个线程调度分配到的时间较少,而且这种线程在阻塞场景下会直接交出整个进程的执行权限。 + +由此可见,用户态线程创建成本低,问题明显,不可以利用多核。 + +内核态线程,创建成本高,可以利用多核,切换速度慢。 + +因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。 + +### 协程 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-586071a1-3d5f-4873-963f-d47b8aeea594.png) + +协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。 + +这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。 + +**子程序** + +或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。 + +所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。 + +子程序调用总是一个入口,一次返回,调用顺序是明确的。 + +**协程的特点在于是一个线程执行,那和多线程比,协程有何优势?** + +* 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显; +* 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。 + +### 线程安全 + +如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。 + +如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 进程 + +在系统中正在运行的一个应用程序;程序一旦运行就是进程;是资源分配的最小单位。 + +在操作系统中能同时运行多个进程; + +开机的时候,磁盘的内核镜像被导入内存作为一个执行副本,成为内核进程。 + +进程可以分成**用户态进程和内核态进程**两类,用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。 + +如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。 + +每个进程都有独立的内存空间,存放代码和数据段等,程序之间的切换会有较大的开销; + +**分时和调度** + +每个进程在执行时都会获得操作系统分配的一个时间片段,如果超出这个时间,就会轮到下一个进程(线程)执行。 + +> 注意,现代操作系统都是直接调度线程,不会调度进程。 + +**分配时间片段** + +如下图所示,进程 1 需要 2 个时间片段,进程 2 只有 1 个时间片段,进程 3 需要 3 个时间片段。 + +因此当进程 1 执行到一半时,会先挂起,然后进程 2 开始执行;进程 2 一次可以执行完,然后进程 3 开始执行,不过进程 3 一次执行不完,在执行了 1 个时间片段后,进程 1 开始执行;就这样如此周而复始,这个就是分时技术。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-606eb7bd-e9e1-457a-bf61-66b152c5ada5.png) + +### 创建进程 + +用户想要创建一个进程,最直接的方法就是从命令行执行一个程序,或者双击打开一个应用,但对于程序员而言,显然需要更好的设计。 + +首先,应该有 API 打开应用,比如可以通过函数打开某个应用; + +另一方面,如果程序员希望执行完一段代价昂贵的初始化过程后,将当前程序的状态复制好几份,变成一个个单独执行的进程,那么操作系统提供了 fork 指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-85a86f1b-eeff-401e-8bfe-19e5509e2a53.png) + +也就是说,每次 fork 会多创造一个克隆的进程,这个克隆的进程,所有状态都和原来的进程一样,但是会有自己的地址空间。 + +如果要创造 2 个克隆进程,就要 fork 两次。 + +> 那如果我就是想启动一个新的程序呢? + +操作系统提供了启动新程序的 API。 + +如果我就是想用一个新进程执行一小段程序,比如说每次服务端收到客户端的请求时,我都想用一个进程去处理这个请求。 + +如果是这种情况,建议你不要单独启动进程,而是使用线程。 + +因为进程的创建成本实在太高了,因此不建议用来做这样的事情:要创建条目、要分配内存,特别是还要在内存中形成一个个段,分成不同的区域。所以通常,我们更倾向于多创建线程。 + +不同程序语言会自己提供创建线程的 API,比如 Java 有 Thread 类;go 有 go-routine(注意不是协程,是线程)。 + +### 进程状态 + +![](http://cdn.tobebetterjavaer.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形式,是针对其他通信机制运行效率较低而设计的。 + +为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间,进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。 + +由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。 + +共享内存示意图: + +![](http://cdn.tobebetterjavaer.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 操作系统采取的便是这种调度算法。 + +> 举例: + +多级队列,就是多个队列执行调度,先考虑最简单的两级模型 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-bcc4bc31-75b9-43f2-9f16-7a0e419e5615.png) + +上图中设计了两个优先级不同的队列,从下到上优先级上升,上层队列调度紧急任务,下层队列调度普通任务。 + +只要上层队列有任务,下层队列就会让出执行权限。 + +低优先级队列可以考虑抢占 + 优先级队列的方式实现,这样每次执行一个时间片段就可以判断一下高优先级的队列中是否有任务。 + +高优先级队列可以考虑用非抢占(每个任务执行完才执行下一个)+ 优先级队列实现,这样紧急任务优先级有个区分,如果遇到十万火急的情况,就可以优先处理这个任务。 + +上面这个模型虽然解决了任务间的优先级问题,但是还是没有解决短任务先行的问题,可以考虑再增加一些队列,让级别更多。 + +> 比如下图这个模型: + +![](http://cdn.tobebetterjavaer.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`的结构。 + +![](http://cdn.tobebetterjavaer.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 的饥饿问题 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 内存管理 + +**逻辑地址和物理地址** + +我们编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。 + +物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址,物理地址是内存单元真正的地址。 + +编译时只需确定变量x存放的相对地址是100 ( 也就是说相对于进程在内存中的起始地址而言的地址)。 + +CPU想要找到x在内存中的实际存放位置,只需要用进程的起始地址+100即可。 + +相对地址又称逻辑地址,绝对地址又称物理地址。 + +**内存管理有哪几种方式** + +1. **块式管理**:将内存分为几个固定大小的块,每个块中只包含一个进程,如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了,这些在每个块中未被利用的空间,我们称之为碎片。 +2. **页式管理**:把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片,页式管理通过页表对应逻辑地址和物理地址。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-2d1e6109-5c99-4ab8-b9b4-00c851386d65.png) + +1. **段式管理**: 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义, 段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 ,段式管理通过段表对应逻辑地址和物理地址。 +2. **段页式管理机制:**段页式管理机制结合了段式管理和页式管理的优点,简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说**段页式管理机制**中段与段之间以及段的内部的都是离散的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-06d985c8-7c5a-46eb-8f04-e7a010a90f40.png) + +### 虚拟地址 + +现代处理器使用的是一种称为**虚拟寻址(Virtual Addressing)**的寻址方式 + +**使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。** + +实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为**内存管理单元(Memory Management Unit, MMU)**的硬件 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-ad8515bb-ce0c-43b3-b9ff-9d4d5fa0994e.png) + +**为什么要有虚拟地址空间** + +没有虚拟地址空间的时候,**程序都是直接访问和操作的都是物理内存**。 + +但是这样有什么问题? + +1. 用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易破坏操作系统,造成操作系统崩溃。 +2. 想要同时运行多个程序特别困难,比如你想同时运行一个微信和一个 QQ 音乐都不行,为什么呢?举个简单的例子:微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就造成了微信这个程序就会崩溃。 + +**通过虚拟地址访问内存有以下优势:** + +- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。 +- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。 +- 不同进程使用的虚拟地址彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。 + +**MMU如何把虚拟地址翻译成物理地址的** + +对于每个程序,内存管理单元MMU都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。 + +每当为一个虚拟页面寻找到一个物理页面之后,就在页表里增加一条记录来保留该映射关系,当然,随着虚拟页面进出物理内存,页表的内容也会不断更新变化。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-169790fd-8ef1-400f-a4a4-e291a729d2cb.png) + +### 虚拟内存 + +很多时候我们使用点开了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存 + +通过 **虚拟内存** 可以让程序可以拥有超过系统物理内存大小的可用内存空间。 + +另外,虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间),这样会更加有效地管理内存并减少出错。 + +**虚拟内存**是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存 + +**虚拟内存的重要意义是它定义了一个连续的虚拟地址空间**,并且 **把内存扩展到硬盘空间** + +**虚拟内存的实现有以下三种方式:** + +1. **请求分页存储管理** :请求分页是目前最常用的一种实现虚拟存储器的方法,请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行,假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。 +2. **请求分段存储管理** :请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。 +3. **请求段页式存储管理** + +不管是上面那种实现方式,我们一般都需要: + +> 一定容量的内存和外存:在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,然后程序就可以执行了; + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-d2104a4e-cb28-4d90-b8f1-92cb15b0bb31.png) + + + +### 缺页中断 + +如果**需执行的指令或访问的数据尚未在内存**(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段**调入到内存**,然后继续执行程序; + +在分页系统中,一个虚拟页面既有可能在物理内存,也有可能保存在磁盘上。 + +如果CPU发出的虚拟地址对应的页面不在物理内存,就将产生一个缺页中断,而缺页中断服务程序负责将需要的虚拟页面找到并加载到内存。 + +缺页中断的处理步骤如下,省略了中间很多的步骤,只保留最核心的几个步骤: + +![](http://cdn.tobebetterjavaer.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 的映射,需要一种叫作页表的结构。 + +![](http://cdn.tobebetterjavaer.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. 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-a9551753-2578-474e-a452-192d01382d22.png) + +### 内存管理单元 + +在 CPU 中一个小型的设备——内存管理单元(MMU) + +![](https://img-blog.csdnimg.cn/2da3a2f130cf415cb0b42c19fda70f30.png"> + +![](http://cdn.tobebetterjavaer.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) + +又称循环首次适应法,由首次适应法演变而成,不同之处是分配内存时从上一次查找结束的位置开始继续查找 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-a072428d-1d01-4c93-b5bb-8d2f9721d2ba.png) + +> 最佳适应算法(Best Fit) + +空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区就进行分配 + +![](https://img-blog.csdnimg.cn/585a5ba3e4ad4eaeae77ebd1d8b02b32.png"> + +> 最坏适应算法(Next Fit) + +又称最大适应算法,空闲分区以容量递减的次序链接,找到第一个能满足要求的空闲分区(也就是最大的分区)就进行分配 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-4673dda6-3940-4a15-ba09-6acfe095ff62.png) + +**总结** + +首次适应不仅最简单,通常也是最好最快,不过首次适应算法会使得内存低地址部分出现很多小的空闲分区,而每次查找都要经过这些分区,因此也增加了查找的开销。 + +邻近算法试图解决这个问题,但实际上,它常常会导致在内存的末尾分配空间分裂成小的碎片,它通常比首次适应算法结果要差。 + +最佳适应算法导致大量碎片,最坏适应算法导致没有大的空间。 + +### 内存覆盖 + +覆盖与交换技术是在程序用来扩充内存的两种方法。 + +早期的计算机系统中,主存容量很小,虽然主存中仅存放一道用户程序,但是存储空间放不下用户进程的现象也经常发生,这一矛盾可以用覆盖技术来解决。 + +**覆盖的基本思想是:** + +由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可以把用户空间分成一个固定区和若干个覆盖区。 + +将经常活跃的部分放在固定区,其余部分按调用关系分段。 + +首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统再将其调入覆盖区,替换覆盖区中原有的段。 + +覆盖技术的特点是打破了必须将一个进程的全部信息装入主存后才能运行的限制,但当同时运行程序的代码量大于主存时仍不能运行。 + +### 内存交换 + +**交换的基本思想** + +把处于等待状态(或在CPU调度原则下被剥夺运行权利)的程序从内存移到辅存,把内存空间腾出来,这一过程又叫换出; + +把准备好竞争CPU运行的程序从辅存移到内存,这一过程又称为换入。 + +> 例如,有一个CPU釆用时间片轮转调度算法的多道程序环境。 + +时间片到,内存管理器将刚刚执行过的进程换出,将另一进程换入到刚刚释放的内存空间中。 + +同时,CPU调度器可以将时间片分配给其他已在内存中的进程。 + +每个进程用完时间片都与另一进程交换。 + +理想情况下,内存管理器的交换过程速度足够快,总有进程在内存中可以执行。 + +> 交换技术主要是在不同进程(或作业)之间进行,而覆盖则用于同一个程序或进程中。 + +由于覆盖技术要求给出程序段之间的覆盖结构,使得其对用户和程序员不透明,所以对于主存无法存放用户程序的矛盾 + +现代操作系统是通过虚拟内存技术来解决的,覆盖技术则已成为历史;而交换技术在现代操作系统中仍具有较强的生命力。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 常见面试题 + +**进程、线程的区别** + +操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。 + +![](http://cdn.tobebetterjavaer.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产生的,同时,它会发送给内核,要求内核处理这些异常** + +下面这张图显示了异常处理的流程: + +![](http://cdn.tobebetterjavaer.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) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cs/wangluo.md b/docs/cs/wangluo.md similarity index 95% rename from docs/src/cs/wangluo.md rename to docs/cs/wangluo.md index 5482d155e5..bf9c1861a9 100644 --- a/docs/src/cs/wangluo.md +++ b/docs/cs/wangluo.md @@ -10,7 +10,7 @@ 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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-608345cf-8378-4b34-bc91-ca6d2fa25da7.png) ## OSI七层模型 @@ -94,7 +94,7 @@ Linux给WIndows发包,不同系统语法不一致,如exe不能在`Linux`下 一层物理层时数据被称为**比特流**(Bits)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-d1fdc5fc-c955-4591-9c95-e297a64eccdc.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-d1fdc5fc-c955-4591-9c95-e297a64eccdc.png) ## TCP和IP模型 @@ -107,7 +107,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 - 第三层:网络层,主要是IP协议。主要负责寻址(找到目标设备的位置) - 第四层:数据链路层,主要是负责转换数字信号和物理二进制信号。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-fa4a4d20-f0db-4ba7-a31e-7772f2f132a8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-fa4a4d20-f0db-4ba7-a31e-7772f2f132a8.png) **四层网络协议的作用** @@ -127,7 +127,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 在数据链路层,对应的协议也会在IP数据包前端加上以太网的部首。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-1d3ce227-fd77-4b95-89dd-aeb786bb4b9e.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-1d3ce227-fd77-4b95-89dd-aeb786bb4b9e.png) 源设备和目标设备通过网线连接,就可以通过物理层的二进制传输数据。 @@ -135,7 +135,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 数据链路层>网络层>传输层>应用层,一层层的解码,最后就可以在浏览器中得到目标设备传送过来的**index.html**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-731f7daa-1b47-4191-828f-c6e54d650604.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-731f7daa-1b47-4191-828f-c6e54d650604.png) **TCP/IP协议族** @@ -145,7 +145,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 具体来说,在网络层是IP/ICMP协议、在传输层是TCP/UDP协议、在应用层是SMTP、FTP、以及 HTTP 等。他们都属于 TCP/IP 协议。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## 网络设备 @@ -179,7 +179,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 它通过识别目的 IP 地址的**网络号**,再根据**路由表**进行数据转发 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## HTTP @@ -374,7 +374,7 @@ HTTPS 协议会对传输的数据进行加密,而加密过程是使用了非 HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过程如下: -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-c1db0431-0eee-4c80-bb60-b7508a306864.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-c1db0431-0eee-4c80-bb60-b7508a306864.png) * Client发起一个HTTPS的请求 * Server把事先配置好的公钥证书返回给客户端。 @@ -395,7 +395,7 @@ HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过 通过数字证书的方式保证服务器公钥的身份,解决冒充的风险。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-45917c49-bb08-4f88-84d2-5e8ae53acc7c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-45917c49-bb08-4f88-84d2-5e8ae53acc7c.png) ### 请求报文 @@ -403,7 +403,7 @@ HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过 HTTP 请求报文由3部分组成(请求行+请求头+请求体) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-3c00598c-43c2-44cd-96c6-ee4d40b97abd.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-3c00598c-43c2-44cd-96c6-ee4d40b97abd.png) **常见的HTTP报文头属性** @@ -431,7 +431,7 @@ HTTP 请求报文由3部分组成(请求行+请求头+请求体) 响应报文与请求报文一样,由三个部分组成(响应行,响应头,响应体) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-58884113-14dc-4cca-a63e-3320f31a4da5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-58884113-14dc-4cca-a63e-3320f31a4da5.png) **HTTP响应报文属性** @@ -445,7 +445,7 @@ HTTP 请求报文由3部分组成(请求行+请求头+请求体) - 服务端可以设置客户端的cookie -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## TCP @@ -499,7 +499,7 @@ TCP的全部功能体现在它首部中各字段的作用 > 1. 首部前20个字符固定、后面有4n个字节是根据需而增加的选项 > 2. 故 TCP首部最小长度 = 20字节 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-8011660d-24c8-460f-ac3d-b97ad9c99b13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-8011660d-24c8-460f-ac3d-b97ad9c99b13.png) **端口**: @@ -565,7 +565,7 @@ TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲 ### 三次握手 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-f6a9438e-4eb8-4573-9ef5-30e07b8c31df.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-f6a9438e-4eb8-4573-9ef5-30e07b8c31df.png) **第一次握手**: @@ -625,7 +625,7 @@ TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲 ### 四次挥手 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-4a5e455f-5cf8-47a6-8fe4-a4c83a445f77.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-4a5e455f-5cf8-47a6-8fe4-a4c83a445f77.png) 挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起: @@ -688,7 +688,7 @@ TCP报文头有个字段叫Window,用于接收方通知发送方自己还有 发送方窗口内的序列号代表了那些已经被发送,但是还没有被确认的帧,或者是那些可以被发送的帧 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-d734c97b-d7d6-4af6-8674-524a81fb4dbe.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-d734c97b-d7d6-4af6-8674-524a81fb4dbe.png) 滑动窗口由四部分组成每个字节的数据都有唯一顺序的编码,随着时间发展,未确认部分与可以发送数据包编码部分向右移动,形式滑动窗口 @@ -717,7 +717,7 @@ TCP报文头有个字段叫Window,用于接收方通知发送方自己还有 主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-4bdc0051-8878-4ffc-b9db-71b73ec49cd0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-4bdc0051-8878-4ffc-b9db-71b73ec49cd0.png) **流量控制引发的死锁** @@ -796,7 +796,7 @@ TCP报文头有个字段叫Window,用于接收方通知发送方自己还有 注意:拥塞避免并非完全能够避免了阻塞,而是使网络比较不容易出现拥塞。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-314eba07-1388-4e8b-9307-8dd8d03b0dfe.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-314eba07-1388-4e8b-9307-8dd8d03b0dfe.png) **快重传算法** @@ -816,19 +816,19 @@ TCP报文头有个字段叫Window,用于接收方通知发送方自己还有 所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-3a424e43-405f-494d-b700-093781b63035.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-eff20ef6-9d35-4075-8c53-ab52c7a46ac7.png) `Socket`不是一种协议,而是一个编程调用接口(`API`),属于传输层(主要解决数据如何在网络中传输) 对用户来说,只需调用Socket去组织数据,以符合指定的协议,即可通信 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## UDP @@ -874,7 +874,7 @@ TFTP、DNS、DHCP、TFTP、SNMP(简单网络管理协议)、RIP基于不可靠 UDP的报文段共有2个字段:数据字段 + 首部字段 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-a5d0d209-01db-4ee7-b63b-bf2659545702.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-a5d0d209-01db-4ee7-b63b-bf2659545702.png) **UDP报文中每个字段的含义如下:** @@ -927,7 +927,7 @@ IP地址表示为:`xxx.xxx.xxx.xxx` 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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-212f3f9b-b07f-4cc9-81eb-55bb478f1b66.png) ### 子网掩码 @@ -971,7 +971,7 @@ IP地址分A、B、C、D、E五类,其中A、B、C这三类是比较常用的I 但是A与C,A与D,B与C,B与D它们之间不属于同一网段,所以它们通信是要经过本地网关,然后路由器根据对方IP地址,在路由表中查找恰好有匹配到对方IP地址的直连路由,于是从另一边网关接口转发出去实现互连 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-e1d68baa-9d8c-4595-bcb5-0dc5f6e240fe.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-e1d68baa-9d8c-4595-bcb5-0dc5f6e240fe.png) **子网掩码和IP地址的关系** @@ -987,7 +987,7 @@ IP地址分A、B、C、D、E五类,其中A、B、C这三类是比较常用的I 将得出的结果转化为十进制,便得到网络地址。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-5fab32f0-20d1-4c05-bfb9-47928ceac65d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-5fab32f0-20d1-4c05-bfb9-47928ceac65d.png) ### 网关 @@ -1051,9 +1051,9 @@ DNS通过主机名,最终得到该主机名对应的IP地址的过程叫做域 将主机域名转换为ip地址,属于应用层协议,使用UDP传输。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-7d129644-dfa7-4151-ae2f-9f3f084c6be9.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-4ed70ed6-ceeb-4761-8fd4-eb2fe092273d.png) 第一步,客户端向本地DNS服务器发送解析请求 @@ -1166,7 +1166,7 @@ ARP即地址解析协议, 用于实现从 IP 地址到 MAC 地址的映射, 源主机收到ARP响应包后,将目的主机的IP和MAC地址写入ARP列表,并利用此信息发送数据,如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## 数字签名 @@ -1259,7 +1259,7 @@ SELECT * FROM user_table WHERE username=’’or 1 = 1 –- and password=’’ **例子**:甲方生成 **一对密钥** 并将其中的一把作为 **公钥** 向其它人公开,得到该公钥的 **乙方** 使用该密钥对机密信息 **进行加密** 后再发送给甲方,甲方再使用自己保存的另一把 **专用密钥** (**私钥**),对 **加密** 后的信息 **进行解密**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## 网络攻击 @@ -1390,7 +1390,7 @@ Session 的**认证过程**: 3. 有效期,Cookie 可以设置任意时间有效,而 Session 一般失效时间短 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## 常见面试题 @@ -1418,4 +1418,4 @@ Session 的**认证过程**: >作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw](https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/download/history.md b/docs/download/history.md similarity index 94% rename from docs/src/download/history.md rename to docs/download/history.md index 8de07f30fa..cadc0e0f9c 100644 --- a/docs/src/download/history.md +++ b/docs/download/history.md @@ -19,6 +19,7 @@ ### 2022年05月31日 - 升级 VuePress-hope 的版本 +- [增加访问权限修饰符](/nice-article/bokeyuan-jianxijavazhongdifangwenquanxiankongzhi.md) ### 2022年05月21日 diff --git a/docs/src/download/javabooks.md b/docs/download/javabooks.md similarity index 97% rename from docs/src/download/javabooks.md rename to docs/download/javabooks.md index 25389e167b..9c569aacc6 100644 --- a/docs/src/download/javabooks.md +++ b/docs/download/javabooks.md @@ -4,7 +4,7 @@ 知乎 CSDN 哔哩哔哩 - 二哥的Java进阶之路 + Java程序员进阶之路 免费PDF

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

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

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

- +
>点击下列目录直接跳转书籍所在类别,但有时目录跳转会失灵...如果没有没有正常跳转可以动动小手向下拉即可,全部的书籍都在本页面。 > ->笔者是一个人在维护这个仓库,本地大概是1100多本书了,需要一步步慢慢上传,我只能在闲暇时间慢慢更新,目前已经更新超过1000+了,如果没有您要用的书,可以加一下 **个人微信**([qing_gee](https://cdn.paicoding.com/tobebetterjavaer/images/qing_gee_noname.jpg)),注明来意,我会慢慢添加上去的。一个人的力量是有限的,请谅解一下。 +>笔者是一个人在维护这个仓库,本地大概是1100多本书了,需要一步步慢慢上传,我只能在闲暇时间慢慢更新,目前已经更新超过1000+了,如果没有您要用的书,可以加一下 **个人微信**([qing_gee](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/qing_gee_noname.jpg)),注明来意,我会慢慢添加上去的。一个人的力量是有限的,请谅解一下。 - [01、入门](#入门) - [02、工具](#工具) @@ -117,7 +117,7 @@ ## 入门 -- 二哥的Java进阶之路 [百度云下载链接](https://pan.baidu.com/s/1UkyKSmQ_oabpY6HJZyl7pw) 密码:v0i5 +- 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 diff --git a/docs/src/download/learn-jianyi.md b/docs/download/learn-jianyi.md similarity index 100% rename from docs/src/download/learn-jianyi.md rename to docs/download/learn-jianyi.md diff --git a/docs/src/download/nicearticle.md b/docs/download/nicearticle.md similarity index 98% rename from docs/src/download/nicearticle.md rename to docs/download/nicearticle.md index cdf95c925b..969a5c959b 100644 --- a/docs/src/download/nicearticle.md +++ b/docs/download/nicearticle.md @@ -91,6 +91,8 @@ - [降薪 45%,从互联网回到国企](https://mp.weixin.qq.com/s/qHGdIuA32X-zydbMTKDPuA) - [学弟在微软的这六个月](https://mp.weixin.qq.com/s/08Ax1ArAjchemjUXih7zNw) - [找个程序员做老公/男票有多爽???](https://mp.weixin.qq.com/s/mK0yaen1mhCoWZ6ZLC5vQg) +- [研究所月入两万,是什么体验?](/manongshenghuo/yanjiusuo-20wan.md) +- [裸辞全职做外包一个月的感受](/manongshenghuo/waibao-1geyue.md) - [转眼就来字节六个月了,真的不一样](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) @@ -204,8 +206,10 @@ > [!TIP] > 开发过程中遇到的一些典型问题,该如何解决? +- [Log4j2突发重大漏洞](/shigu/log4j2.md) - [重现了一波 Log4j2 核弹级漏洞,同事的电脑沦为炮灰](https://mp.weixin.qq.com/s/zXzJVxRxMUnoyJs6_NojMQ) - [生成订单30分钟未支付,则自动取消,该怎么实现?](https://mp.weixin.qq.com/s/J6jb_Dt3C49CIjYBTrN4gQ) +- [两天两夜,1M图片优化到100kb!](/shigu/image-yasuo.md) - [内部群炸了锅,隔壁同事真删库了啊。。](https://mp.weixin.qq.com/s/lvyoN1gHCWhcPqudcjcRgQ) - [B 站崩了](https://mp.weixin.qq.com/s/PfJe5rXednkXTq8EKT0xpw) - [因为一个低级错误,生产数据库崩溃了将近半个小时](https://mp.weixin.qq.com/s/ec6u8WsPt7zJ0eul8sPEhg) @@ -223,6 +227,7 @@ ## MySQL重要知识点 +- [从京东到家程序员删库被判 10 个月来聊聊 MySQL 数据备份的杀手锏](/mysql/deletedb-binlog-weiguanjishu.md) - [深入浅出MySQL crash safe](https://tech.youzan.com/shen-ru-qian-chu-mysql-crash-safe/) ## 待收录文章 diff --git a/docs/elasticsearch/rumen.md b/docs/elasticsearch/rumen.md new file mode 100644 index 0000000000..8b5eaaef7e --- /dev/null +++ b/docs/elasticsearch/rumen.md @@ -0,0 +1,189 @@ +--- +category: + - Java企业级开发 +tag: + - Elasticsearch +--- + +# 全文搜索引擎Elasticsearch入门教程 + +学习真的是一件令人开心的事情,上次分享了 [Redis 入门](https://mp.weixin.qq.com/s/NPJkMy5RppyFk9QhzHxhrw)的文章后,收到了很多小伙伴的鼓励,比如说:“哎呀,不错呀,二哥,通俗易懂,十分钟真的入门了”。瞅瞅,瞅瞅,我决定再接再厉,入门一下 Elasticsearch,因为我们公司的商城系统升级了,需要用 Elasticsearch 做商品的搜索。 + +不过,我首先要声明一点,我对 Elasticsearch 并没有进行很深入的研究,仅仅是因为要用,就学一下。但作为一名负责任的技术博主,我是用心的,为此还特意在某某时间上买了一门视频课程,作者叫阮一鸣。说实话,他光秃秃的头顶让我对这门课程产生了浓厚的兴趣。 + +经过三天三夜的学习,总算是入了 Elasticsearch 的门,我就决定把这些心得体会分享出来,感兴趣的小伙伴可以作为参考。遇到文章中有错误的地方,不要手下留情,过来捶我,只要不打脸就好。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-ebb2bdbc-2cdb-4540-b48f-41f92c848f2f.jpg) + + +### 01、Elasticsearch 是什么 + +>Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。 + +以上引用来自于官方,不得不说,解释得蛮文艺的。意料之中和意料之外,这两个词让我想起来了某一年的高考作文题(情理之中和意料之外)。 + +Elastic Stack 又是什么呢?整个架构图如下图(来源于网络,侵删)所示。 + +![](http://cdn.tobebetterjavaer.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 包解压就可以了。 + +![](http://cdn.tobebetterjavaer.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 服务了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-7dd19afd-1aeb-49b6-a07c-f11e139fe3d3.jpg) + +输出的日志信息有点多,不用细看,注意看到有“started”的字样就表明启动成功了。为了进一步确认 Elasticsearch 有没有启动成功,可以在浏览器的地址栏里输入 `http://localhost:9200` 进行查看(9200 是 Elasticsearch 的默认端口号)。 + +![](http://cdn.tobebetterjavaer.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 颗葡萄吃,狗头。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-12372ee6-acc0-4425-964b-ca32886f17ce.jpg) + +包目录不再一一解释了,进入 bin 目录下,双击运行 kibana.bat 文件,启动 Kibana 服务。整个过程比 Elasticsearch 要慢一些,当看到 `[Kibana][http] http server running` 的信息后,说明服务启动成功了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-784d70ef-b6e7-4312-85f1-36ace9b2a5bd.jpg) + +在浏览器地址栏输入 `http://localhost:5601` 查看 Kibana 的图形化界面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-e6f64545-a925-4bb4-a25e-44129832fb4e.jpg) + +由于当前的 Elasticsearch 服务端中还没有任何数据,所以我们可以选择「Try Our Sample Data」导入 Kibana 提供的模拟数据体验一下。下图是导入电商数据库的看板页面,是不是很丰富? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-a16d99ff-272d-43bb-aa94-23b240cc464b.jpg) + +打开 Dev Tools 面板,可以看到一个简单的 DSL 查询语句(一种完全基于 JSON 的特定于领域的语言),点击「运行」按钮后就可以看到 JSON 格式的数据了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-5c44bd79-d3a9-49fb-9414-04dc38840cfb.jpg) + +### 04、Elasticsearch 的关键概念 + +在进行下一步之前,需要先来理解 Elasticsearch 中的几个关键概念,比如说什么是索引,什么是类型,什么是文档等等。Elasticsearch 既然是一个数据引擎,它里面的一些概念就和 MySQL 有一定的关系。 + +![](http://cdn.tobebetterjavaer.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”索引,结果如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-64baa243-0075-436e-a070-f28813fee284.jpg) + + + + +### 06、鸣谢 + +好了,我亲爱的小伙伴们,以上就是本文的全部内容了,是不是看完后很想实操一把 Elasticsearch,赶快行动吧!如果你在学习的过程中遇到了问题,欢迎随时和我交流,虽然我也是个菜鸟,但我有热情啊。 + +另外,如果你想写入门级别的文章,这篇就是最好的范例。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/exception/gailan.md b/docs/exception/gailan.md new file mode 100644 index 0000000000..361fcfd1f1 --- /dev/null +++ b/docs/exception/gailan.md @@ -0,0 +1,475 @@ +--- +title: 一文彻底搞懂Java异常处理,YYDS +shortTitle: 一文彻底搞懂Java异常处理 +category: + - Java核心 +tag: + - 异常处理 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,一文彻底搞懂Java异常处理,YYDS +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,异常处理 +--- + +## 一、什么是异常 + +“二哥,今天就要学习异常了吗?”三妹问。 + +“是的。只有正确地处理好异常,才能保证程序的可靠性,所以异常的学习还是很有必要的。”我说。 + +“那到底什么是异常呢?”三妹问。 + +“异常是指中断程序正常执行的一个不确定的事件。当异常发生时,程序的正常执行流程就会被打断。一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了。” + +“有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程。” + +“除此之外,异常处理机制可以保证我们向用户提供友好的提示信息,而不是程序原生的异常信息——用户根本理解不了。” + +“不过,站在开发者的角度,我们更希望看到原生的异常信息,因为这有助于我们更快地找到 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的区别 + +“哦,我知道了。下一个问题,我经常看到一些文章里提到 Exception 和 Error,二哥你能帮我解释一下它们之间的区别吗?”三妹问。 + +“这是一个好问题呀,三妹!” + +从单词的释义上来看,error 为错误,exception 为异常,错误的等级明显比异常要高一些。 + +从程序的角度来看,也的确如此。 + +Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。 + +Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。 + +比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。 + +## 三、checked和unchecked异常 + +“三妹,还能想到其他的问题吗?” + +“嗯,不用想,二哥,我已经提前做好预习工作了。”三妹自信地说,“异常又可以分为 checked 和 unchecked,它们之间又有什么区别呢?” + +“哇,三妹,果然又是一个好问题呢。” + +checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出。 + +“我先画一幅思维导图给你感受一下。” + +![](http://cdn.tobebetterjavaer.com/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`。 + +![](http://cdn.tobebetterjavaer.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 虚拟机来处理,但我觉得这样做肯定更不合适。 + +## 四、关于 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 中。 + +![](http://cdn.tobebetterjavaer.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 关键字后面跟的是异常的对象。 + +示例。 + +``` +throws ArithmeticException; +``` + +``` +throw new ArithmeticException("算术异常"); +``` + + 3)throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。 + +4)throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。 + +## 五、关于 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` 之前,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` 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。 + +“三妹,来看一下源码的文档注释就全明白了!” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/exception/try-catch-finally-01.png) + +至于参数 status 的值也很好理解,如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/exception/npe.md b/docs/exception/npe.md similarity index 90% rename from docs/src/exception/npe.md rename to docs/exception/npe.md index 1f86928b00..2c06792df0 100644 --- a/docs/src/exception/npe.md +++ b/docs/exception/npe.md @@ -1,15 +1,15 @@ --- title: Java空指针NullPointerException的传说 -shortTitle: 空指针的传说 +shortTitle: NPE的传说 category: - Java核心 tag: - 异常处理 -description: 本文详细解析了Java编程中的空指针异常(NullPointerException)现象,从产生原因、常见场景到预防方法,为您揭开NullPointerException的神秘面纱。文章还提供了实际代码示例,帮助您理解如何在实际开发中避免空指针异常,提高代码的健壮性和可靠性。 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java空指针NullPointerException的传说 head: - - meta - name: keywords - content: java,npe,NullPointerException + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,npe,NullPointerException --- @@ -21,13 +21,13 @@ head: ... ... -我叫王二,我来到这个奇怪的世界已经一年了,我等了一年,穿越附赠的老爷爷、戒指、系统什么的我到现在都没发现。 +我叫铁柱,我来到这个奇怪的世界已经一年了,我等了一年,穿越附赠的老爷爷、戒指、系统什么的我到现在都没发现。 而且这个世界看起来也太奇怪了,这里好像叫什么 **Java** 大陆,我只知道这个世界的最强者叫做 **Object**,听说是什么道祖级的存在,我也不知道是什么意思,毕竟我现在好像还是个菜鸡,别的主角一年都应该要飞升仙界了吧,我还连个小火球都放不出来。 哦,对了,上面的那段话是我在茶馆喝茶的时候听说书的先生说的,总觉得空指针这个名字怪怪的,好像在什么地方听说过。 -我的头痛的毛病又犯了,我已经记不起来我为什么来到这里了,我只记得我的名字叫王二,其他的,我只感觉这个奇怪的世界有一种熟悉,但是我什么都记不起来了。 +我的头痛的毛病又犯了,我已经记不起来我为什么来到这里了,我只记得我的名字叫铁柱,其他的,我只感觉这个奇怪的世界有一种熟悉,但是我什么都记不起来了。 算了,得过且过吧。 @@ -177,7 +177,7 @@ Object听到这话,皱了皱眉,他沉默了一会儿,缓缓站起身子 我见他好像魔怔了,仿佛在思考什么,于是迈步走到他刚才站立的地方看着前面,原来,这是他们的族谱!这里是异常的祠堂! -![](https://cdn.paicoding.com/tobebetterjavaer/images/exception/npe-1.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/exception/npe-1.jpg) 看完这张族谱,我恍然大悟,好像明白了什么。突然,我的脑袋里出现了一个冰冷的机器声音:“获取异常族谱,历练完成度+100。” @@ -223,14 +223,13 @@ Object听到这话,皱了皱眉,他沉默了一会儿,缓缓站起身子 可是,他为什么要给我,看他刚才的样子都想打我了,又突然给了我这些?还有他一直在说的规则之力又是什么?这座城市为什么又这么诡异? ->转载链接:[https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg](https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg),作者:艾小仙 +>转载链接:[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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/exception/shijian.md b/docs/exception/shijian.md new file mode 100644 index 0000000000..10829251bf --- /dev/null +++ b/docs/exception/shijian.md @@ -0,0 +1,433 @@ +--- +title: Java异常处理的20个最佳实践 +shortTitle: Java异常处理的20个最佳实践 +category: + - Java核心 +tag: + - 异常处理 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java异常处理的20个最佳实践 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,异常处理 +--- + + +“三妹啊,今天我来给你传授几个异常处理的最佳实践经验,以免你以后在开发中采坑。”我面带着微笑对三妹说。 + +“好啊,二哥,我洗耳恭听。”三妹也微微一笑,欣然接受。 + +“好,那哥就不废话了。开整。” + +-------- + +## 尽量不要捕获 RuntimeException + +阿里出品的嵩山版 Java 开发手册上这样规定: + +>尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。 + +正例: + +```java +if (obj != null) { + //... +} +``` + +反例: + +```java +try { + obj.method(); +} catch (NullPointerException e) { + //... +} +``` + +“哦,那如果有些异常预检查不出来呢?”三妹问。 + +“的确会存在这样的情况,比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理。”我说。 + +## 尽量使用 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); + } + } + } +} +``` + +## 不要捕获 Throwable + +Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。 + +```java +public void doNotCatchThrowable() { + try { + } catch (Throwable t) { + // 不要这样做 + } +} +``` + +“到底为什么啊?”三妹问。 + +“因为有些 error 是不需要程序来处理,程序可能也处理不了,比如说 OutOfMemoryError 或者 StackOverflowError,前者是因为 Java 虚拟机无法申请到足够的内存空间时出现的非正常的错误,后者是因为线程申请的栈深度超过了允许的最大深度出现的非正常错误,如果捕获了,就掩盖了程序应该被发现的严重错误。”我说。 + +“打个比方,一匹马只能拉一车厢的货物,拉两车厢可能就挂了,但一 catch,就发现不了问题了。”我补充道。 + +## 不要省略异常信息的记录 + +很多时候,由于疏忽大意,开发者很容易捕获了异常却没有记录异常信息,导致程序上线后真的出现了问题却没有记录可查。 + +```java +public void doNotIgnoreExceptions() { + try { + } catch (NumberFormatException e) { + // 没有记录异常 + } +} +``` + +应该把错误信息记录下来。 + +```java +public void logAnException() { + try { + } catch (NumberFormatException e) { + log.error("哦,错误竟然发生了: " + e); + } +} +``` + +## 不要记录了异常又抛出了异常 + +这纯属画蛇添足,并且容易造成错误信息的混乱。 + +反例: + +```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); + } +} +``` + +这种也是一样的道理,既然已经捕获了,就不要在方法签名上抛出了。 + +## 不要在 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 了。”三妹说。 + +“是这样的。”我点点头。 + +## 在你的方法里抛出定义具体的检查性异常 + +``` +public void foo() throws Exception { //错误方式 +} +``` + +一定要避免出现上面的代码示例,它破坏了检查性异常的目的。 声明你的方法可能抛出的具体检查性异常,如果只有太多这样的检查性异常,你应该把它们包装在你自己的异常中,并在异常消息中添加信息。 如果可能的话,你也可以考虑代码重构。 + +``` +public void foo() throws SpecificException1, SpecificException2 { //正确方式 +} +``` + +## 捕获具体的子类而不是捕获 Exception 类 + +``` +try { + someMethod(); +} catch (Exception e) { //错误方式 + LOGGER.error("method has failed", e); +} +``` + +捕获异常的问题是,如果稍后调用的方法为其方法声明添加了新的检查性异常,则开发人员的意图是应该处理具体的新异常。如果你的代码只是捕获异常(或 Throwable),永远不会知道这个变化,以及你的代码现在是错误的,并且可能会在运行时的任何时候中断。 + +## 在你的方法里抛出定义具体的检查性异常 + +``` +public void foo() throws Exception { //错误方式 +} +``` + +一定要避免出现上面的代码示例,它破坏了检查性异常的目的。 声明你的方法可能抛出的具体检查性异常,如果只有太多这样的检查性异常,你应该把它们包装在你自己的异常中,并在异常消息中添加信息。 如果可能的话,你也可以考虑代码重构。 + +``` +public void foo() throws SpecificException1, SpecificException2 { //正确方式 +} +``` + + +## 始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失 + +``` +catch (NoSuchMethodException e) { + throw new MyServiceException("Some information: " + e.getMessage()); //错误方式 +} +``` + +这破坏了原始异常的堆栈跟踪,并且始终是错误的,正确的做法是: + +``` +catch (NoSuchMethodException e) { + throw new MyServiceException("Some information: " , e); //正确方式 +} +``` + + +## finally 块中永远不要抛出任何异常 + +``` +try { + someMethod(); //Throws exceptionOne +} finally { + cleanUp(); //如果finally还抛出异常,那么exceptionOne将永远丢失 +} +``` + +只要 cleanUp() 永远不会抛出任何异常,上面的代码没有问题,但是如果 someMethod() 抛出一个异常,并且在 finally 块中,cleanUp() 也抛出另一个异常,那么程序只会把第二个异常抛出来,原来的第一个异常(正确的原因)将永远丢失。如果在 finally 块中调用的代码可能会引发异常,请确保要么处理它,要么将其记录下来。永远不要让它从 finally 块中抛出来。 + + + +## 不要使用 printStackTrace() 语句或类似的方法 + +完成代码后,切勿忽略 printStackTrace(),最终别人可能会得到这些堆栈,并且对于如何处理它完全没有任何方法,因为它不会附加任何上下文信息。 + +## 对于不打算处理的异常,直接使用 finally + +``` +try { + someMethod(); //Method 2 +} finally { + cleanUp(); //do cleanup here +} +``` + +这是一个很好的做法,如果在你的方法中你正在访问 Method 2,而 Method 2 抛出一些你不想在 Method 1 中处理的异常,但是仍然希望在发生异常时进行一些清理,然后在 finally 块中进行清理,不要使用 catch 块。 + +## 记住早 throw 晚 catch 原则 + +这可能是关于异常处理最著名的原则,简单说,应该尽快抛出(throw)异常,并尽可能晚地捕获(catch)它。应该等到有足够的信息来妥善处理它。 + +这个原则隐含地说,你将更有可能把它放在低级方法中,在那里你将检查单个值是否为空或不适合。而且你会让异常堆栈跟踪上升好几个级别,直到达到足够的抽象级别才能处理问题。 + + +## 只抛出和方法相关的异常 + +相关性对于保持应用程序清洁非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException,那么它不会给用户任何相关的信息。相反,如果这种异常被包裹在自定义异常中,则会更好。NoSuchFileFoundException 则对该方法的用户更有用。 + +## 切勿在程序中使用异常来进行流程控制 + +不要在项目中出现使用异常来处理应用程序逻辑。永远不要这样做,它会使代码很难阅读和理解。 + +## 尽早验证用户输入以在请求处理的早期捕获异常 + +始终要在非常早的阶段验证用户输入,甚至在达到 controller 之前,它将帮助你把核心应用程序逻辑中的异常处理代码量降到最低。如果用户输入出现错误,还可以保证与应用程序一致。 + +例如:如果在用户注册应用程序中,遵循以下逻辑: + +1. 验证用户 +2. 插入用户 +3. 验证地址 +4. 插入地址 +5. 如果出问题回滚一切 + +这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后将用户数据置于 dao 层并进行数据库更新。正确的做法是: + +1. 验证用户 +2. 验证地址 +3. 插入用户 +4. 插入地址 +5. 如果问题回滚一切 + +## 一个异常只能包含在一个日志中 + +``` +LOGGER.debug("Using cache sector A"); +LOGGER.debug("Using retry sector B"); +``` + +不要像上面这样做,对多个 LOGGER.debug() 调用使用多行日志消息可能在你的测试用例中看起来不错,但是当它在具有 100 个并行运行的线程的应用程序服务器的日志文件中显示时,所有信息都输出到相同的日志文件,即使它们在实际代码中为前后行,但是在日志文件中这两个日志消息可能会间隔 100 多行。应该这样做: + +``` +LOGGER.debug("Using cache sector A, using retry sector B"); +``` + +## 将所有相关信息尽可能地传递给异常 + +有用的异常消息和堆栈跟踪非常重要,如果你的日志不能定位异常位置,那要日志有什么用呢? + +## 终止掉被中断线程 + +``` +while (true) { + try { + Thread.sleep(100000); + } catch (InterruptedException e) {} //别这样做 + doSomethingCool(); +} +``` + +InterruptedException 异常提示应该停止程序正在做的事情,比如事务超时或线程池被关闭等。 + +应该尽最大努力完成正在做的事情,并完成当前执行的线程,而不是忽略 InterruptedException。修改后的程序如下: + +``` +while (true) { + try { + Thread.sleep(100000); + } catch (InterruptedException e) { + break; + } +} +doSomethingCool(); +``` + +## 对于重复的 try-catch,使用模板方法 + +在代码中有许多类似的 catch 块是无用的,只会增加代码的重复性,针对这样的问题可以使用模板方法。 + +例如,在尝试关闭数据库连接时的异常处理。 + +``` +class DBUtil{ + public static void closeConnection(Connection conn){ + try{ + conn.close(); + } catch(Exception ex){ + //Log Exception - Cannot close connection + } + } +} +``` + +这类的方法将在应用程序很多地方使用。不要把这块代码放的到处都是,而是定义上面的方法,然后像下面这样使用它: + +``` +public void dataAccessCode() { + Connection conn = null; + try{ + conn = getConnection(); + .... + } finally{ + DBUtil.closeConnection(conn); + } +} +``` + +## 使用 JavaDoc 中记录应用程序中的所有异常 + +把用 JavaDoc 记录运行时可能抛出的所有异常作为一种习惯,其中也尽量包括用户应该遵循的操作,以防这些异常发生。 + +--- + +“好了,三妹,关于异常处理实践就先讲这 20 条吧,实际开发中你还会碰到其他的一些坑,自己踩一踩可能印象更深刻一些。”我说。 + +“那万一到时候我工作后被领导骂了怎么办?”三妹委屈地说。 + +“新人嘛,总要写几个 bug 才能对得起新人这个称号嘛。”我轻描淡写地说。 + +“好吧。”三妹无奈地叹了口气。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + + + + diff --git a/docs/exception/try-with-resouces.md b/docs/exception/try-with-resouces.md new file mode 100644 index 0000000000..9e0ee7b0d0 --- /dev/null +++ b/docs/exception/try-with-resouces.md @@ -0,0 +1,312 @@ +--- +title: 深入理解 Java 中的 try-with-resouces 语法糖 +shortTitle: try-with-resouces +category: + - Java核心 +tag: + - 异常处理 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,深入理解 Java 中的 try-with-resouces 语法糖 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java,try-with-resouces +--- + +“二哥,终于等到你讲 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。前者产生的代码更加简洁、清晰,产生的异常信息也更靠谱。” + +“靠谱!”三妹说。 + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + diff --git a/docs/src/git/git-qiyuan.md b/docs/git/git-qiyuan.md similarity index 87% rename from docs/src/git/git-qiyuan.md rename to docs/git/git-qiyuan.md index ed60af3df6..642ae2b12d 100644 --- a/docs/src/git/git-qiyuan.md +++ b/docs/git/git-qiyuan.md @@ -1,24 +1,25 @@ --- -title: 1小时彻底掌握 Git,(可能是)史上最简单明了的 Git 教程 -shortTitle: 最简单明了的 Git 教程 category: - - 开发/构建工具 + - Java企业级开发 tag: - Git -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,1小时彻底掌握 Git,(可能是)史上最简单明了的 Git 教程 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Git入门,Git教程,git +shortTitle: Git --- +# 我在工作中是如何使用 Git 的 + ## 一、Git 起源 Git 是一个分布式版本控制系统,缔造者是大名鼎鼎的林纳斯·托瓦茲 (Linus Torvalds),Git 最初的目的是为了能更好的管理 Linux 内核源码。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-01.png) + +PS:**为了能够帮助更多的 Java 爱好者,已将《Java 程序员进阶之路》开源到了 GitHub(本篇已收录)。如果你也喜欢这个专栏,觉得有帮助的话,可以去点个 star,这样也方便以后进行更系统化的学习**: +[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +*每天看着 star 数的上涨我心里非常的开心,希望越来越多的 Java 爱好者能因为这个开源项目而受益,而越来越多人的 star,也会激励我继续更新下去*~ 大家都知道,Linux 内核是开源的,参与者众多,到目前为止,共有两万多名开发者给 Linux Kernel 提交过代码。 @@ -54,7 +55,7 @@ Junio Hamano 觉得 Linus 设计的这些命令对于普通用户不太友好, 如今,Git 已经成为全球软件开发者的标配。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-02.png) 原本的 Git 只适用于 Unix/Linux 平台,但随着 Cygwin、msysGit 环境的成熟,以及 TortoiseGit 这样易用的GUI工具,Git 在 Windows 平台下也逐渐成熟。 @@ -65,7 +66,7 @@ Junio Hamano 觉得 Linus 设计的这些命令对于普通用户不太友好, Git 和传统的版本控制工具 CVS、SVN 有不小的区别,前者关心的是文件的整体性是否发生了改变,后两者更关心文件内容上的差异。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-03.png) 除此之外,Git 更像是一个文件系统,每个使用它的主机都可以作为版本库,并且不依赖于远程仓库而离线工作。开发者在本地就有历史版本的副本,因此就不用再被远程仓库的网络传输而束缚。 @@ -88,7 +89,7 @@ Git 中的绝大多数操作都只需要访问本地文件和资源,一般不 Git 的工作流程是这样的: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-04.png) - 在工作目录中修改文件 @@ -102,11 +103,11 @@ Git 的工作流程是这样的: >https://git-scm.com/downloads -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-05.png) 我个人使用的 macOS 系统,可以直接使用 `brew install git` 命令安装,非常方便。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-06.png) 安装成功后,再使用 `git --version` 就可以查看版本号了,我本机上安装的是 2.23.0 版本。 @@ -287,7 +288,7 @@ TREE: 目录树对象。在 Linus 的设计里,TREE 对象就是一个时间 另外,由于 TREE 上记录文件名及属性信息,对于修改文件属性或修改文件名、移动目录而不修改文件内容的情况,可以复用 BLOB 对象,节省存储资源。而 Git 在后来的开发演进中又优化了 TREE 的设计,变成了某一时间点文件夹信息的抽象,TREE 包含其子目录的 TREE 的对象信息(SHA1)。这样,对于目录结构很复杂或层级较深的 Git 库 可以节约存储资源。历史信息被记录在第三种对象 CHANGESET 里。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/neibushixian-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/neibushixian-01.png) CHANGESET:即 Commit 对象。一个 CHANGESET 对象中记录了该次提交的 TREE 对象信息(SHA1),以及提交者(committer)、提交备注(commit message)等信息。 @@ -304,7 +305,7 @@ Linus 解释了“当前目录缓存”的设计,该缓存就是一个二进 - 1. 能够快速的复原缓存的完整内容,即使不小心把当前工作区的文件删除了,也可以从缓存中恢复所有文件; - 2. 能够快速找出缓存中和当前工作区内容不一致的文件。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/neibushixian-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/neibushixian-02.png) Linus 在 Git 的第一次代码提交里便完成了 Git 的最基础功能,并可以编译使用。代码极为简洁,加上 Makefile 一共只有 848 行。感兴趣的话可以通过上一段所述方法 checkout Git 最早的 commit 上手编译玩玩,只要有 Linux 环境即可。 @@ -332,7 +333,7 @@ Linus 在 Git 的第一次代码提交里便完成了 Git 的最基础功能, 一般来说,日常使用只要记住下图中这 6 个命令就可以了,但是熟练使用 Git,恐怕要记住60~100个命令~ -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/mingling-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/mingling-01.png) @@ -347,7 +348,7 @@ Linus 在 Git 的第一次代码提交里便完成了 Git 的最基础功能, 下面是阮一峰老师整理的常用 Git 命令清单,有必要的话,可以打印一份出来,放在工作台~ ->[http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html](http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html) +>http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html ### 1、新建代码库 @@ -641,7 +642,7 @@ $ git archive 但是,太方便了也会产生副作用。如果你不加注意,很可能会留下一个枝节蔓生、四处开放的版本库,到处都是分支,完全看不出主干发展的脉络。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-01.png) 那有没有一个好的分支策略呢?答案当然是有的。 @@ -650,7 +651,7 @@ $ git archive 首先,代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-02.png) Git主分支的名字,默认叫做Master。它是自动建立的,版本库初始化以后,默认就是在主分支在进行开发。 @@ -658,7 +659,7 @@ Git主分支的名字,默认叫做Master。它是自动建立的,版本库 主分支只用来发布重大版本,日常开发应该在另一条分支上完成。我们把开发用的分支,叫做Develop。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-03.png) 这个分支可以用来生成代码的最新隔夜版本(nightly)。如果想正式对外发布,就在Master分支上,对Develop分支进行"合并"(merge)。 @@ -680,11 +681,11 @@ Git创建Develop分支的命令: 这里稍微解释一下上一条命令的--no-ff参数是什么意思。默认情况下,Git执行"快进式合并"(fast-farward merge),会直接将Master分支指向Develop分支。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-04.png) 使用--no-ff参数后,会执行正常合并,在Master分支上生成一个新节点。为了保证版本演进的清晰,我们希望采用这种做法。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-05.png) ### 3、临时性分支 @@ -702,7 +703,7 @@ Git创建Develop分支的命令: **第一种是功能分支**,它是为了开发某种特定功能,从Develop分支上面分出来的。开发完成后,要再并入Develop。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-06.png) 功能分支的名字,可以采用feature-*的形式命名。 @@ -755,7 +756,7 @@ Git创建Develop分支的命令: 修补bug分支是从Master分支上面分出来的。修补结束以后,再合并进Master和Develop分支。它的命名,可以采用fixbug-*的形式。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-07.png) 创建一个修补bug分支: ``` @@ -791,7 +792,7 @@ Git创建Develop分支的命令: 新建一个文件夹,比如说 testgit,然后使用 `git init` 命令就可以把这个文件夹初始化为 Git 仓库了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-01.png) 初始化Git 仓库成功后,可以看到多了一个 .git 的目录,没事不要乱动,免得破坏了 Git 仓库的结构。 @@ -802,17 +803,17 @@ Git创建Develop分支的命令: 第二步,使用 `git commit` 命令告诉 Git,把文件提交到仓库。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-02.png) 可以使用 `git status` 来查看是否还有文件未提交。 也可以在文件中新增一行内容“传统美德不能丢,记得点赞哦~”,再使用 `git status` 来查看结果。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-03.png) 如果想查看文件到底哪里做了修改,可以使用 `git diff` 命令: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-04.png) 确认修改的内容后,可以使用 `git add` 和 `git commit` 再次提交。 @@ -822,11 +823,11 @@ Git创建Develop分支的命令: 现在我已经对 readme.txt 文件做了三次修改了。可以通过 `git log` 命令来查看历史记录: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-05.png) 也可以通过 `gitk` 这个命令来启动图形化界面来查看版本历史。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-06.png) 如果想回滚的话,比如说回滚到上一个版本,可以执行以下两种命令: @@ -835,15 +836,15 @@ Git创建Develop分支的命令: 2)`git reset --hard HEAD~100`,如果回滚到前 100 个版本,用这个命令比上一个命令更方便。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-07.png) 那假如回滚错了,想恢复,不记得版本号了,可以先执行 `git reflog` 命令查看版本号: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-08.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-08.png) 然后再通过 `git reset --hard` 命令来恢复: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-09.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-09.png) ### 3、工作区和暂存区的区别 @@ -865,7 +866,7 @@ Git 在提交文件的时候分两步,第一步 `git add` 命令是把文件 原子性带来的好处是显而易见的,这使得我们可以把项目整体还原到某个时间点,就这一点,SVN 就完虐 CVS 这些代码版本管理系统了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-10.png) Git 作为逼格最高的代码版本管理系统,自然要借鉴 SVN 这个优良特性的。但不同于 SVN 的是,Git 一开始搞的都是命令行,没有图形化界面,如果想要像 SVN 那样一次性选择多个文件或者不选某些文件(见上图),还真特喵的是个麻烦事。 @@ -881,7 +882,7 @@ Git 作为逼格最高的代码版本管理系统,自然要借鉴 SVN 这个 我们先用 `git status` 命令查看一下状态,再用 `git add` 将文件添加到暂存区,最后再用 `git commit` 一次性提交到 Git 仓库。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-11.png) ### 4、撤销修改 @@ -895,17 +896,17 @@ Git 作为逼格最高的代码版本管理系统,自然要借鉴 SVN 这个 答案当然是有了,其实在我们执行 `git status` 命令查看 Git 状态的时候,结果就提示我们可以使用 `git restore` 命令来撤销这次操作的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-12.png) 那其实在 git version 2.23.0 版本之前,是可以通过 `git checkout` 命令来完成撤销操作的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-13.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-14.png) ### 5、远程仓库 @@ -923,7 +924,7 @@ Git 是一款分布式版本控制系统,所以同一个 Git 仓库,可以 **第一步,通过 `ls -al ~/.ssh` 命令检查 SSH 密钥是否存在** -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-15.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-15.png) 如果没有 id_rsa.pub、id_ecdsa.pub、id_ed25519.pub 这 3 个文件,表示密钥不存在。 @@ -937,21 +938,21 @@ ssh-keygen -t ed25519 -C "your_email@example.com" 然后一路回车: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-16.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-16.png) 记得复制一下密钥,在 id_ed25519.pub 文件中: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-17.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-18.png) Title 可不填写,提交成功后会列出对应的密钥: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-19.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-19.png) **为什么 GitHub 需要 SSH 密钥呢**? @@ -961,17 +962,17 @@ Title 可不填写,提交成功后会列出对应的密钥: 点击新建仓库,填写仓库名称等信息: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-20.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-20.png) **第五步,把本地仓库同步到 GitHub** 复制远程仓库的地址: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-21.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-21.png) 在本地仓库中执行 `git remote add` 命令将 GitHub 仓库添加到本地: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-22.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-22.png) 当我们第一次使用Git 的 push 命令连接 GitHub 时,会得到一个警告⚠️: @@ -985,28 +986,28 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 接下来,我们使用 `git push` 命令将当前本地分支推送到 GitHub。加上了 -u 参数后,Git 不但会把本地的 master 分支推送的远程 master 分支上,还会把本地的 master 分支和远程的master 分支关联起来,在以后的推送或者拉取时就可以简化命令(比如说 `git push github master`)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-23.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-23.png) 此时,我们刷一下 GitHub,可以看到多了一个 master 分支,并且本地的两个文件都推送成功了! -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-24.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-25.png) 然后我使用 `git clone` 命令将其拷贝到本地。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-26.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-26.png) ## 八、详解 sparse-checkout 命令 -前天不是搭建了一个《二哥的Java进阶之路》的网站嘛,其中用到了 Git 来作为云服务器和 GitHub 远程仓库之间的同步工具。 +前天不是搭建了一个《Java 程序员进阶之路》的网站嘛,其中用到了 Git 来作为云服务器和 GitHub 远程仓库之间的同步工具。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-01.png) @@ -1017,9 +1018,9 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes ### 1、使用 Git 中遇到的一个大麻烦 -首先给大家通报一下,一天前[上线的《二哥的Java进阶之路》网站](https://javabetter.cn),目前访问次数已经突破 1000 了。 +首先给大家通报一下,一天前[上线的《Java 程序员进阶之路》网站](https://tobebetterjavaer.com),目前访问次数已经突破 1000 了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-03.png) 正所谓**不积跬步无以至千里,不积小流无以成江海**。 @@ -1034,7 +1035,7 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 大家可以先看一下我这个 GitHub 仓库的目录结构哈。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-04.png) - docs 是文档目录,里面是 md 文件,所有的教程原稿都在这里。 - codes 是代码目录,里面是教程的配套源码。 @@ -1042,11 +1043,11 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 这样就可以利用 GitHub 来做免费的图床,并且还可以白票 jsDelivr CDN 的全球加速,简直不要太爽! -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-05.png) 比如说 images 目录下有一张 logo 图 logo-01.png: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-06.png) 如果使用 GitHub 仓库的原始路径来访问的话,速度贼慢! @@ -1054,7 +1055,7 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 使用 jsDelivr 加速后就不一样了,速度飞起! ->https://cdn.paicoding.com/tobebetterjavaer/images/logo-01.png +>http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/logo-01.png 简单总结下 GitHub 作为图床的正确用法,就两条: @@ -1064,7 +1065,7 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 付费七牛云或者阿里云图床的小伙伴不妨试试这种方式,能白票咱绝不花一分冤枉钱。 -那也就是说,《二哥的Java进阶之路》网站上的图片都是通过 GitHub 图床加载的,不需要将图片从 GitHub 仓库拉取到云服务器上。要知道,一台云服务器的空间是极其昂贵的,能省的空间咱必须得省。 +那也就是说,《Java 程序员进阶之路》网站上的图片都是通过 GitHub 图床加载的,不需要将图片从 GitHub 仓库拉取到云服务器上。要知道,一台云服务器的空间是极其昂贵的,能省的空间咱必须得省。 ### 2、学习 Git 中遇到的一个大惊喜 @@ -1074,16 +1075,16 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 最后还是浏览 Git 官方手册(也可以看[Pro Git](https://mp.weixin.qq.com/s/RpFzXOa2VlFNd7ylLmr9LQ))才找到了一个牛逼的命令:**git sparse-checkout,它可以帮助我们在拉取远程仓库的时候只同步那些我们想要的目录和文件**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-07.png) 具体怎么用,可以看官方文档: ->[https://git-scm.com/docs/git-sparse-checkout](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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-08.png) 第二步,启用 sparse-checkout,并初始化 @@ -1091,30 +1092,30 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 然后再执行 `git sparse-checkout init` 初始化。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-09.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-09.png) 第三步,使用 sparse-checkout 来拉取我们想要的仓库目录 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-10.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-12.png) 第四步,验证是否生效 可以执行 `ls -al` 命令来确认 sparse-checkout 是否生效。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-13.png) 如图所示,确实只拉取到了 docs 目录。 假如还想要拉取其他文件或者目录的话,可以通过 `git sparse-checkout add` 命令来添加。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-14.png) 这就实现了,**远程仓库和云服务器仓库之间的定制化同步,需要什么目录和文件就同步什么目录和文件,不需要的可以统统不要**。 @@ -1122,13 +1123,13 @@ GitHub 仓库可以免费用,空间也无限大,但云服务可是要抠抠 我对比了一下,远程仓库大概 145 M,图片就占了 72 M,妥妥地省下了一半的存储空间。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-15.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-16.png) 可以看到,那些我们不想要的目录和文件统统都又回来了。 @@ -1136,7 +1137,7 @@ GitHub 仓库可以免费用,空间也无限大,但云服务可是要抠抠 也简单,只需要执行一下 `git sparse-checkout reapply` 命令就可以了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-17.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-17.png) 简单总结下:如果你要把一个庞大到撑满你硬盘的远程仓库拉取到本地,而你只需要其中的一部分目录和文件,那就可以试一试 `git sparse-checkout` 了。 @@ -1147,7 +1148,7 @@ GitHub 仓库可以免费用,空间也无限大,但云服务可是要抠抠 不得不说,Git 实在是太强大了。就一行命令,解决了困扰我一天的烦恼,我的 80G 存储空间的云服务器又可以再战 3 年了,从此以后再也不用担心了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-18.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-18.png) Git 是真的牛逼,Linus 是真的牛逼,神不愧是神! @@ -1161,16 +1162,9 @@ Git 是真的牛逼,Linus 是真的牛逼,神不愧是神! 参考资料: ->- 维基百科:[https://zh.wikipedia.org/wiki/Git](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/git/progit.md b/docs/git/progit.md new file mode 100644 index 0000000000..a7ac49e746 --- /dev/null +++ b/docs/git/progit.md @@ -0,0 +1,38 @@ +今天给大家分享一本个人最近看过觉得非常不错的Git开源手册,可能有些小伙伴也看过了,我是最近在通勤路上用PAD看的。这本开源手册,它除了有**PDF版**,还有**epub电子书版**,非常适合电子阅读,有需要的小伙伴可以在文末自行下载: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-01.png) + +相信看完对于个人Git知识体系的梳理和掌握是非常有帮助的。 + +这本手册在豆瓣上评价极高,之前9.3,现在也有9.1的高分,其作者是GitHub的员工,内容主要侧重于各种场合中的惯用法和底层原理的讲述,手册中还针对不同的使用场景,设计了几个合适的版本管理策略。简而言之,这本手册无论是对于初学者还是想进一步了解Git工作原理的开发者都非常合适。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-02.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-03.png) + +这个手册一共分为十章,详细内容如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-04.png) + +**手册中部分内容展示如下:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-05.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-06.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-07.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-08.png) + +**需要该Git手册PDF+epub电子书的小伙伴:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-09.png) + +可直接长按扫码关注下方二维码,回复 「**git**」 即可下载: + +![(长按扫码识别)](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/itwanger.png) + + +好了,这次资源分享就到这里!后续如果遇到有用的工具或者资源,依然还会持续分享,也欢迎大家多多安利和交流,一起分享成长。 + +以上,我们下篇见。 diff --git a/docs/gongju/DBeaver.md b/docs/gongju/DBeaver.md new file mode 100644 index 0000000000..20404da358 --- /dev/null +++ b/docs/gongju/DBeaver.md @@ -0,0 +1,171 @@ +--- +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) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-1.png) + +DBeaver 支持几乎所有主流的数据库,包括关系型数据库和非关系数据库。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-2.png) + +## 二、安装 DBeaver + +可以通过 DBeaver 官方下载安装包,也可以通过 GitHub 下载 release 版本。 + +>官方下载地址:[https://dbeaver.io/download/](https://dbeaver.io/download/) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-3.png) + +根据自己电脑的操作系统下载对应的安装包,完整安装后,第一步要做的是配置 Maven 镜像,否则在后续下载数据库驱动的时候会非常的慢。 + +![](http://cdn.tobebetterjavaer.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 镜像一样,如下图所示。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-5.png) + +配置完成后,记得把阿里云镜像仓库置顶。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-6.png) + + +## 三、管理数据源 + +像使用 Navicat 一样,我们需要先建立连接,这里就以 MySQL 为例。点击「连接」小图标,选择数据库。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-7.png) + +点击下一步,这时候需要填写数据库连接信息。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-8.png) + +点击「测试链接」,如果使用默认的 Maven 仓库时,下载驱动会非常慢,如下图所示,还容易失败「踩过的坑就不要再踩了」。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-9.png) + +如果你前面按照我说的配置了阿里云的 Maven 镜像,程序就不一样了,点了「测试链接」,瞬间会弹出「连接已成功」的提示框。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-10.png) + +链接成功后,就可以看到数据库中的表啊、视图啊、索引啊等等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-11.png) + +## 四、管理表 + +数据库连接成功后,最重要的还是操作表。 + +**01、查看表** + +选择一张表,双击后就可以看到表的属性了,可以查看表的列、约束(主键)、外键、索引等等信息。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-12.png) + +点击「DDL(Data Definition Language,数据定义语言)」可以看到详细的建表语句。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-13.png) + +点击「数据」可以查看表的数据,底部有「新增」、「修改」、「删除」等行操作按钮。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-14.png) + +可以在顶部的过滤框中填写筛选条件,然后直接查询结果。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-15.png) + +如果不想显示某一列的话,可以直接点击「自定义结果集」图表,将某个字段的状态设置为不可见即可。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-16.png) + +**02、新增表** + +在左侧选择「表」,然后右键选择「新建表」即可建表id。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-17.png) + +之后在右侧列的区域右键,选择「新建列」即可添加字段。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-18.png) + +比如说我们新建一个主键 ID,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-19.png) + +在 DBeaver 中,`[v]` 表示真,`[]` 表示否。紧接着在「约束」里选择 ID 将其设置为主键。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-20.png) + +最后点击保存,会弹出一个建表语句的预览框,点击「执行」即可完成表的创建。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-21.png) + +## 五、执行 SQL + +右键数据库表,选择右键菜单中的「SQL 编辑器」可以打开 SQL 编辑面板。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-22.png) + +然后编辑 SQL 语句,点击运行的小图标就可以查询数据了。这个过程会有语法提示,非常 nice。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-23.png) + +DBeaver 有一个很亮眼的操作就是,可以直接选中一条结果集,然后右键生成 SQL。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-24.png) + +比如说 insert 语句,这样再插入一条重复性内容的时候就非常方便了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-25.png) + +## 六、外观配置 + +可以在首选项里对外观进行设置,比如说把主题修改为暗黑色。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-26.png) + +然后界面就变成了暗黑系。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-27.png) + +还可以设置字体大小等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-28.png) + +从整体的风格来看,DBeaver 和 Eclipse 有些类似,事实上也的确如此,DBeaver 是基于 Eclipse 平台构建的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-29.png) + +## 七、总结 + +总体来说,DBeaver是一款非常优秀的开源数据库管理工具了,功能很全面,日常的开发基本上是够用了。对比收费的 Navicat 和 DataGrip,可以说非常良心了。大家如果遇到收费版不能使用的时候,可以来体验一下社区版 DBeaver。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/gongju/brew.md b/docs/gongju/brew.md similarity index 75% rename from docs/src/gongju/brew.md rename to docs/gongju/brew.md index 5e87b32609..dc76dcfb4e 100644 --- a/docs/src/gongju/brew.md +++ b/docs/gongju/brew.md @@ -14,9 +14,9 @@ head: ## 前言(废话) -本来打算在公司偷偷摸摸给星球的用户写一篇编程喵整合 MongoDB 的文章,结果在通过 brew 安装 MongoDB 的时候竟然报错了。原因很简单,公司这台 Mac 上的 homebrew 环境没有配置好。刚好 二哥的Java进阶之路上缺少这样一篇内容。 +本来打算在公司偷偷摸摸给星球的用户写一篇编程喵整合 MongoDB 的文章,结果在通过 brew 安装 MongoDB 的时候竟然报错了。原因很简单,公司这台 Mac 上的 homebrew 环境没有配置好。刚好 Java 程序员进阶之路上缺少这样一篇内容。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-c6756a26-8767-4135-be4c-b31d42de2a89.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-c6756a26-8767-4135-be4c-b31d42de2a89.png) 所以我就想,不如趁机水一篇吧,啊,不不不,趁机给小伙伴们普及一下 Homebrew 吧!瞧我这该死的大公无私的心(手动狗头)。 @@ -29,7 +29,7 @@ head: Homebrew 这款命令行软件管理神器在 GitHub 上已经有 32.5k+ 的 star 了,功能也真心强大,几乎 macOS 上的软件包它都包了。Homebrew 本身没有问题,问题在于。。。。。不说了,你懂的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-726f97d9-4de2-4d23-9973-d1a39951a0f7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-726f97d9-4de2-4d23-9973-d1a39951a0f7.png) Homebrew 除了是 macOS 的包管理器也可以装在 Linux 上成为 Linux 的包管理器,仅需要执行相应的命令,就能下载安装需要的软件包,省去了下载、解压、拖拽等繁琐的步骤。 @@ -79,36 +79,36 @@ Homebrew 主要由四个部分组成: brew、homebrew-core 、homebrew-cask、ho 安装脚本里提供了中科大、清华大学、北京外国语大学、腾讯、阿里巴巴等下载源。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-e7eaf096-7477-4ed0-814a-07ef8d62884f.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-33bcd6a1-d843-4a6c-a8ac-0d908095da79.png) - `brew ls` 查看本地命令; - `brew search mongodb` 查找软件; @@ -118,7 +118,7 @@ nice,安装完成了。执行下面这几个命令体验下。 - `brew config` 查看配置。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-ac0e141f-301c-46f1-bd80-5375eb50dc4d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-ac0e141f-301c-46f1-bd80-5375eb50dc4d.png) @@ -129,23 +129,23 @@ nice,安装完成了。执行下面这几个命令体验下。 以前的版本中,是可以直接 `brew cask list` 这样执行命令的,现在改成了 `brew list --cask`。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-c5403959-01fd-4610-b08c-4ca4e4cb0a66.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-441127b1-f7ae-484e-801b-5b808f8e283c.png) - 「Formulae」一般是那些命令行工具、开发库、字体、插件等不含 GUI 界面的软件。 - 「Cask」就会包含一些 GUI 图形化界面的软件,如 Google Chrome、FireFox 、Atom 等 @@ -186,20 +186,20 @@ brew tap [user/repo] | 将开源仓库添加到源 2)执行 `brew install openjdk@17` 安装 JDK。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-b4d7684b-2db8-4286-bf2e-389c2aed5968.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-2e10e772-1944-474a-bbfa-b3ef3e0ec9d4.png) 安装: @@ -221,13 +221,13 @@ 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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-a32accec-4044-480c-a8c8-3781bc5048b5.png) 管理: @@ -237,7 +237,7 @@ jenv global 17.0.3 ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-cc01fad8-53e9-4474-8923-08e97ac7090a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-cc01fad8-53e9-4474-8923-08e97ac7090a.png) 是不是贼方便?再也不用整这 `echo 'export PATH="/usr/local/opt/openjdk@17/bin:$PATH"' >> ~/.zshrc` 玩意了!爽,实在是爽! @@ -254,7 +254,7 @@ brew tap mongodb/brew ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-4819ca75-01e3-4dea-8859-7c9ddd570142.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-4819ca75-01e3-4dea-8859-7c9ddd570142.png) 2)更新 brew @@ -272,7 +272,7 @@ brew install mongodb-community@4.4 OK,安装成功。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-e934f3f3-c7de-4537-8c30-73c0e9fb41b4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-e934f3f3-c7de-4537-8c30-73c0e9fb41b4.png) ## 小结 @@ -280,10 +280,10 @@ brew install mongodb-community@4.4 这里顺带给大家提一则小故事,教别人学算法的大佬可以拿走了:homebrew 的作者去面 Google,被考算法题: 反转二叉树,结果没通过被拒了,😆 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-8fb73388-ffaf-4241-8584-03e7aa00578b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/brew-8fb73388-ffaf-4241-8584-03e7aa00578b.png) 好了好了,今天这篇文章就先水到这吧,我们下期见~ -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/gongju/chiner.md b/docs/gongju/chiner.md new file mode 100644 index 0000000000..f34de320d6 --- /dev/null +++ b/docs/gongju/chiner.md @@ -0,0 +1,167 @@ +--- +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,界面漂亮,功能强大,体验后给我的感觉是真香...... + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-1.png) + + +## 一、关于 PowerDesigner + +PowerDesigner 是一款功能非常强大的建模工具,可以和 Rational Rose 媲美。Rose 专攻 UML 对象模型的建模,之后才拓展到数据库这块。而 PowerDesigner 是一开始就为数据库建模服务的,后来才发展为一款综合战斗力都还不错的建模工具。 + +不过,说句实在话,PowerDesigner 的界面偏古典一些,下面是我用 PowerDesigner 设计 DB 的效果。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-2.png) + +## 二、关于 chiner + +chiner,发音:[kaɪˈnər],使用React+Electron+Java技术体系构建的一款元数建模平台。 + +2018 年,作者和几个对开源有兴趣的社区好友开始打磨产品的原因,历经三代,直到 2021 年 7 月份,终于推出了船新的 3.0 版本。 + +2019 年底,团队差点解散,幸好有几位好友关照,给了团队两个项目做,这才算是熬了过去。 + +不得不说,做任何一件事情都不容易啊,光靠情怀也许可以撑过产品初期,但越往后去,遇到生存问题时,就会非常困难。 + +在此,我们必须得为每一位开源作者奉上最真诚的掌声,希望他们的产品都能有一番天地。也希望,未来我的产品出现在大家的面前时,能给它多一点点包容和支持。 + +![](http://cdn.tobebetterjavaer.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 为例。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-4.png) + +安装完成后首次打开的样子是这样的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-5.png) + +chiner 提供了非常贴心的操作手册和参考模板,如果时间比较充分的话,可以先把操作手册过一遍,写得非常详细。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-6.png) + +## 四、上手 chiner + +### **01、导入导出** + +因为我之前有一份 PowerDesigner 文件,所以可以直接导入到 chiner。 + +第一步,新建一个项目 codingmore。 + +第二步,选择导入 PowerDesigner 文件。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-7.png) + +第三步,选择要添加的数据表。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-8.png) + +第四步,导入完成后,就可以点开单表进行查看了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-9.png) + +第五步,当完成重新设计后,就可以选择导出 DDL 到数据库表了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-10.png) + +当然了,也可以直接配置数据库 DB,这样就可以直接连接导入导出了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-11.png) + +导出的 SQL 文件可以直接通过宝塔面板上传到服务器端,然后再直接导入到数据库。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-12.png) + +如果需要用到数据库说明文档的话,也可以直接通过导出到 Word 文档来完成。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-13.png) + +### **02、维护数据类型** + +chiner 自带了几种常见的数据类型,比如字串、小数、日期等,我们也可以根据自己的需要添加新的数据类型。 + +比如说默认的字串类型关联到其他数据库的类型如下所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-14.png) + +数据域是在数据类型的基础上,基于当前项目定义的有一定业务含义的数据类型,比如说我这里维护了一个长度为 90 的名称数据域。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-15.png) + +当我需要把某个数据字段的数据域设置成「名称」的时候,长度就会自动填充为 90,不需要手动再去设置。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-16.png) + +### **03、维护数据表** + +第一步,选中数据表,右键选择「新增数据表」 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-17.png) + +第二步,填写数据表名 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-18.png) + +点击「确定」后,chiner 会帮我们自动生成一些常见常用的字段,比如说创建人、创建时间、更新人、更新时间等,非常的智能化。通常来说,这些字段都是必须的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-19.png) + +如果这些默认字段不满足需求的时候,还可以点击「设置」新增默认字段,比如说删除标记,一般来说为了安全起见,数据库都会采用非物理删除。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-20.png) + +一般来说,我们更习惯字段小写命名,因此可以直接选中一列,然后选择大小写转换。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-21.png) + +就变成小写了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-22.png) + +### **04、维护关系图** + +第一步,选择「关系图」,右键选择「新增关系图」 + +第二步,把需要关联的表拖拽到右侧的面板当中,然后按照字段进行连线,非常的方便。比如说班级和学院表、班级和专业表的关系,就如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-23.png) + +来看一下整体给出来的关系图,还是非常清爽的。 + +![](http://cdn.tobebetterjavaer.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) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/gongju/choco.md b/docs/gongju/choco.md new file mode 100644 index 0000000000..4092f1f3af --- /dev/null +++ b/docs/gongju/choco.md @@ -0,0 +1,184 @@ +--- +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,就像这样。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-474773ad-69eb-467d-acd8-1928ebf27e3a.png) + +然后这样配置环境变量。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-c463c792-60a8-4d16-8cba-dcbe1ece1453.png) + +结果他是这样的,就一行命令,环境变量也不用配置! + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-340c54de-c793-4bbc-9112-96977f8ec69a.png) + +卧槽!牛逼高大上啊! + +看着他熟练地在命令行里安装 JDK 的样子,我的嘴角开始微微上扬,真不错!这次总算招到了一个靠谱的。 + +于是我就安排他做一个记录,打算发表在我的小破站《Java 程序员进阶之路》上。从他嘴里了解到,他用的命令行软件管理器叫 chocolatey,这是一个Windows下的命令行软件管理器,在 GitHub 上已经收获 8.2k+的星标,可以方便开发者像在Linux下使用yum命令来安装软件,或者像在macOS下使用brew 命令来安装软件,非常酷炫。 + + +![](http://cdn.tobebetterjavaer.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 ..` 返回上级目录。 + + +![](http://cdn.tobebetterjavaer.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 下都可以使用上下箭头来选择以往执行过的命令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-269f4133-cdd3-414f-baf9-31067e0eb27f.png) + +- 命令历史,macOS/Linux 下可以使用 `history` 命令查看所有使用过的命令。Windows 可以按下 F7 键。 + + +![](http://cdn.tobebetterjavaer.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 命令行。 + + +![](http://cdn.tobebetterjavaer.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')) +``` + +稍等片刻,就完成安装了。 + +安装完成后如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-2cfc4656-e996-4678-bd57-29cc78587e73.png) + +如果不确定是否安装成功的话,可以通过键入 `choco` 命令来确认。 + +![](http://cdn.tobebetterjavaer.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。 + +![](http://cdn.tobebetterjavaer.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是否安装成功了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-ddc37a22-43d7-4e40-bcfd-7208f9d1df59.png) + +不得不承认!非常nice! + +再比如说安装 Redis,只需要找到 Redis 的安装命令在 Choco 下执行一下就 OK 了。 + + +![](http://cdn.tobebetterjavaer.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 上的软件包也非常的多,基本上软件开发中常见的安装包都有。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-0f43e407-68ab-4c2d-8fb9-7fb88ca638ec.png) + + +### 小结 + +通过小二的实战笔记,我们可以了解到。 + +对比下载安装包,通过图形化界面的方式安装 JDK,然后下一步,下一步是不是感觉在 Shell 下安装 JDK 更炫酷一些? + +关键是还省去了环境变量的配置。 + +记得还没有走出新手村的时候,就经常被环境变量配置烦不胜烦。那下载这种命令行的方式,要比手动在环境变量中配置要省事一百倍,也更不容易出错。 + +通过 Choco 可以集中安装、管理、更新各种各样的软件。特别适合管理一些轻量级的开源软件,一条命令搞定,升级的时候也方便,不用再重新去下载新的安装包,可以有效治愈更新强迫症患者的症状。 + +如果不想特殊设置的话,Chocolatey 整体的操作与使用还是比较亲民的。就连刚接触软件开发的小白也可以直接使用,而且路人看着会觉得你特别厉害。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + diff --git a/docs/src/gongju/fastjson.md b/docs/gongju/fastjson.md similarity index 93% rename from docs/src/gongju/fastjson.md rename to docs/gongju/fastjson.md index dcc69913bb..c5e36e795b 100644 --- a/docs/src/gongju/fastjson.md +++ b/docs/gongju/fastjson.md @@ -12,7 +12,7 @@ tag: 我是 fastjson,是个地地道道的杭州土著,但我始终怀揣着一颗走向全世界的雄心。这不,我在 GitHub 上的简介都换成了英文,国际范十足吧? -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-0576767f-c447-49f1-83a3-6971782c4d52.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-0576767f-c447-49f1-83a3-6971782c4d52.png) 如果你的英语功底没有我家老板 666 的话,我可以简单地翻译下(说人话,不装逼)。 @@ -233,7 +233,7 @@ public class IdentityHashMap { 再比如说,使用 asm 技术来避免反射导致的开销。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-86a38cb0-3acc-4132-8e1f-48ebeaa52b47.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-86a38cb0-3acc-4132-8e1f-48ebeaa52b47.png) 我承认,快的同时,也带来了一些安全性的问题。尤其是 AutoType 的引入,让黑客有了可乘之机。 @@ -257,14 +257,14 @@ public class IdentityHashMap { 在于黑客的反复较量中,我虽然变得越来越稳重成熟了,但与此同时,让我的用户为此也付出了沉重的代价。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-6868c673-8799-4326-baab-1050a5a4e9a3.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-85a44233-6eb2-4164-a091-6b65fc5f001a.png) 最令我感到为之动容的一句话是: @@ -274,7 +274,7 @@ public class IdentityHashMap { 为了彻底解决 AutoType 带来的问题,在 1.2.68 版本中,我引入了 safeMode 的安全模式,无论白名单和黑名单,都不支持 AutoType,这样就可以彻底地杜绝攻击。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-57146979-cb99-4236-94f9-1cd5276e8269.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-57146979-cb99-4236-94f9-1cd5276e8269.png) 安全模式下,`checkAutoType()` 方法会直接抛出异常。 @@ -285,4 +285,4 @@ public class IdentityHashMap { 2020 年的最后一篇文章!看到的就点个赞吧,2021 年顺顺利利。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/forest.md b/docs/gongju/forest.md similarity index 97% rename from docs/src/gongju/forest.md rename to docs/gongju/forest.md index f282f7c0b8..762db306ae 100644 --- a/docs/src/gongju/forest.md +++ b/docs/gongju/forest.md @@ -141,11 +141,11 @@ Forest 的字面意思是森林的意思,更内涵点的话,可以拆成For **虽然 star 数还不是很多,但 star 趋势图正在趋于爬坡阶段,大家可以拿来作为一个练手项目,我觉得还是不错的选择**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/forest-55b54f3f-88a7-458b-b8e0-b0d60e916d5e.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/forest-41345eec-fe16-4fcf-9448-0cb8c57d515f.png) 前端部分: @@ -382,4 +382,4 @@ myClient.send("foo", (String resText, ForestRequest request, ForestResponse resp 这篇文章不仅介绍了 Forest 这个轻量级的 HTTP 客户端框架,还回顾了它的底层实现:HttpClient 和 OkHttp,希望能对大家有所帮助。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/gson.md b/docs/gongju/gson.md similarity index 97% rename from docs/src/gongju/gson.md rename to docs/gongju/gson.md index ce70a00711..67653fb6f9 100644 --- a/docs/src/gongju/gson.md +++ b/docs/gongju/gson.md @@ -243,7 +243,7 @@ class Bar{ 假如你 debug 的时候,进入到 `toJson()` 方法的内部,就可以观察到。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/gson-402ff6b5-a460-45de-ab62-ede6fbf6b61e.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/gson-402ff6b5-a460-45de-ab62-ede6fbf6b61e.png) foo 的实际类型为 `Foo`,但我女朋友在调用 `foo.getClass()` 的时候,只会得到 Foo,这就意味着她并不知道 foo 的实际类型。 @@ -283,11 +283,11 @@ Bar bar1 = foo1.get(); debug 进入 `toJson()` 方法内部查看的话,就可以看到 foo 的真实类型了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/gson-1c6eac43-6f0b-4a00-ae6c-2db29f911719.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/gson-5afe5cd1-4966-4b16-adcb-fc04edfff406.png) 这样的话,bar1 就可以通过 `foo1.get()` 到了。 @@ -455,4 +455,4 @@ private int age = 18; 如果你觉得我有点用的话,不妨点个赞,留个言,see you。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/jackson.md b/docs/gongju/jackson.md similarity index 98% rename from docs/src/gongju/jackson.md rename to docs/gongju/jackson.md index c2ab9fef63..5bde839e17 100644 --- a/docs/src/gongju/jackson.md +++ b/docs/gongju/jackson.md @@ -19,7 +19,7 @@ Java 之所以牛逼,很大的功劳在于它的生态非常完备,JDK 没 当我们通过 starter 新建一个 Spring Boot 的 Web 项目后,就可以在 Maven 的依赖项中看到 Jackson 的身影。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/jackson-4340975c-e254-4287-88e0-66f73fe88889.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/jackson-4340975c-e254-4287-88e0-66f73fe88889.png) Jackson 有很多优点: @@ -47,7 +47,7 @@ Jackson 的核心模块由三部分组成: 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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/jackson-24990211-7a18-44d7-aff0-6ac9e3cf0561.png) Maven 之所以讨人喜欢的一点就在这,能偷偷摸摸地帮我们把该做的做了。 @@ -563,4 +563,4 @@ Woman{age=18, name='三妹'} 好了,通过这篇文章的系统化介绍,相信你已经完全摸透 Jackson 了,我们下篇文章见。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/junit.md b/docs/gongju/junit.md similarity index 91% rename from docs/src/gongju/junit.md rename to docs/gongju/junit.md index d472e52fbb..daeb3dfac5 100644 --- a/docs/src/gongju/junit.md +++ b/docs/gongju/junit.md @@ -16,7 +16,7 @@ tag: 微软公司之前有这样一个统计:bug 在单元测试阶段被发现的平均耗时是 3.25 小时,如果遗漏到系统测试则需要 11.5 个小时。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-5b0afb32-c60e-4218-98b1-44288705e472.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-5b0afb32-c60e-4218-98b1-44288705e472.png) 经我这么一说,你应该已经很清楚单元测试的重要性了。那在你最初编写测试代码的时候,是不是经常这么做?就像下面这样。 @@ -56,17 +56,17 @@ public class Factorial { 第一步,直接在当前的代码编辑器窗口中按下 `Command+N` 键(Mac 版),在弹出的菜单中选择「Test...」。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-fe29e8b8-9264-4aa3-9139-6ebb39af88a1.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-91bf986d-3586-4175-9ca2-959e5eb62e9c.png) 导入完毕后,你可以打开 pom.xml 文件确认一下,里面多了对我的依赖。 @@ -95,11 +95,11 @@ void fact() { 第三步,你可以在邮件菜单中选择「Run FactorialTest」来运行测试用例,结果如下所示。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-7daf1a3d-a321-4d42-9d16-134043161a29.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-5b71c36f-684d-4d30-b1a1-faef453603ae.png) 不难吧?单元测试可以确保单个方法按照正确的预期运行,如果你修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动是没有问题的。 @@ -124,7 +124,7 @@ public class Calculator { 新建测试用例的时候记得勾选`setUp` 和 `tearDown`。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-afa3c969-a2d2-439c-b440-5b7480592d52.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-afa3c969-a2d2-439c-b440-5b7480592d52.png) 生成后的代码如下所示。 @@ -307,4 +307,4 @@ void fromJava9to11() { 希望我能尽早的替你发现代码中的 bug,毕竟越早的发现,造成的损失就会越小。see you! -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/knife4j.md b/docs/gongju/knife4j.md similarity index 77% rename from docs/src/gongju/knife4j.md rename to docs/gongju/knife4j.md index 2ffa9d1599..974303a51d 100644 --- a/docs/src/gongju/knife4j.md +++ b/docs/gongju/knife4j.md @@ -7,7 +7,7 @@ tag: --- -一般在使用 Spring Boot 开发前后端分离项目的时候,都会用到 [Swagger](https://javabetter.cn/springboot/swagger.html)(戳链接详细了解)。 +一般在使用 Spring Boot 开发前后端分离项目的时候,都会用到 [Swagger](https://tobebetterjavaer.com/springboot/swagger.html)(戳链接详细了解)。 但随着系统功能的不断增加,接口数量的爆炸式增长,Swagger 的使用体验就会变得越来越差,比如请求参数为 JSON 的时候没办法格式化,返回结果没办法折叠,还有就是没有提供搜索功能。 @@ -19,15 +19,15 @@ Knife4j 的前身是 swagger-bootstrap-ui,是 springfox-swagger-ui 的增强 U springfox-swagger-ui 的界面长这个样子,说实话,确实略显丑陋。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-1.png) swagger-bootstrap-ui 增强后的样子长下面这样。单纯从直观体验上来看,确实增强了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-2.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-2.png) 那改良后的 Knife4j 不仅在界面上更加优雅、炫酷,功能上也更加强大:后端 Java 代码和前端 UI 模块分离了出来,在微服务场景下更加灵活;还提供了专注于 Swagger 的增强解决方案。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-3.png) 官方文档: @@ -102,7 +102,7 @@ public class Knife4jController { } ``` -第四步,由于 springfox 3.0.x 版本 和 Spring Boot 2.6.x 版本有冲突,所以还需要先解决这个 bug,一共分两步(在[Swagger](https://javabetter.cn/springboot/swagger.html) 那篇已经解释过了,这里不再赘述,但防止有小伙伴在学习的时候再次跳坑,这里就重复一下步骤)。 +第四步,由于 springfox 3.0.x 版本 和 Spring Boot 2.6.x 版本有冲突,所以还需要先解决这个 bug,一共分两步(在[Swagger](https://tobebetterjavaer.com/springboot/swagger.html) 那篇已经解释过了,这里不再赘述,但防止有小伙伴在学习的时候再次跳坑,这里就重复一下步骤)。 先在 application.yml 文件中加入: @@ -157,17 +157,17 @@ public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { >访问地址(和 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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-3cfbf598-b94a-4081-aab3-06af1eef612c.png) 简单来介绍下 Knife4j 的 功能特点: @@ -176,47 +176,47 @@ public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { Knife4j 和 Swagger 一样,也是支持头部登录认证的,点击「authorize」菜单,添加登录后的信息即可保持登录认证的 token。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-6.png) 如果某个 API 需要登录认证的话,就会把之前填写的信息带过来。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-7.png) **2)支持 JSON 折叠** Swagger 是不支持 JSON 折叠的,当返回的信息非常多的时候,界面就会显得非常的臃肿。Knife4j 则不同,可以对返回的 JSON 节点进行折叠。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-8.png) **3)离线文档** Knife4j 支持把 API 文档导出为离线文档(支持 markdown 格式、HTML 格式、Word 格式), -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-9.png) 使用 Typora 打开后的样子如下,非常的大方美观。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-10.png) **4)全局参数** 当某些请求需要全局参数时,这个功能就很实用了,Knife4j 支持 header 和 query 两种方式。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-11.png) 之后进行请求的时候,就会把这个全局参数带过去。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-12.png) **5)搜索 API 接口** Swagger 是没有搜索功能的,当要测试的接口有很多的时候,当需要去找某一个 API 的时候就傻眼了,只能一个个去拖动滚动条去找。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-13.png) 在文档的右上角,Knife4j 提供了文档搜索功能,输入要查询的关键字,就可以检索筛选了,是不是很方便? -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-14.png) 目前支持搜索接口的地址、名称和描述。 @@ -226,11 +226,11 @@ Swagger 是没有搜索功能的,当要测试的接口有很多的时候,当 >[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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-15.png) ---- -更多内容,只针对《二哥的Java进阶之路》星球用户开放,需要的小伙伴可以[戳链接🔗](https://javabetter.cn/zhishixingqiu/)加入我们的星球,一起学习,一起卷。。**编程喵**🐱是一个 Spring Boot+Vue 的前后端分离项目,融合了市面上绝大多数流行的技术要点。通过学习实战项目,你可以将所学的知识通过实践进行检验、你可以拓宽自己的技术边界,你可以掌握一个真正的实战项目是如何从 0 到 1 的。 +更多内容,只针对《Java 程序员进阶之路》星球用户开放,需要的小伙伴可以[戳链接🔗](https://tobebetterjavaer.com/zhishixingqiu/)加入我们的星球,一起学习,一起卷。。**编程喵**🐱是一个 Spring Boot+Vue 的前后端分离项目,融合了市面上绝大多数流行的技术要点。通过学习实战项目,你可以将所学的知识通过实践进行检验、你可以拓宽自己的技术边界,你可以掌握一个真正的实战项目是如何从 0 到 1 的。 ---- @@ -240,4 +240,4 @@ Swagger 是没有搜索功能的,当要测试的接口有很多的时候,当 > - 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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/log4j.md b/docs/gongju/log4j.md similarity index 94% rename from docs/src/gongju/log4j.md rename to docs/gongju/log4j.md index 308091e8b7..d7d046eb93 100644 --- a/docs/src/gongju/log4j.md +++ b/docs/gongju/log4j.md @@ -12,7 +12,7 @@ tag: 这不,我在战国时代读者群里发现了这么一串聊天记录: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-cacd9e88-4a4d-4127-a18b-f99b2e2296a3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-cacd9e88-4a4d-4127-a18b-f99b2e2296a3.png) 竟然有小伙伴不知道“打日志”是什么意思,不知道该怎么学习,还有小伙伴回答说,只知道 Log4j! @@ -22,7 +22,7 @@ tag: (说好的不在乎,怎么在乎起来了呢?手动狗头) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-58282c4d-8178-45bd-8ba3-26740f6dd4a3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-58282c4d-8178-45bd-8ba3-26740f6dd4a3.png) 管他呢,**我行我素**吧,保持初心不改就对了!这篇文章就来说说 Log4j,这个打印日志的鼻祖。Java 中的日志打印其实是个艺术活,我保证,这句话绝不是忽悠。 @@ -37,7 +37,7 @@ tag: 之所以这样打印日志,是因为很方便,上手难度很低,尤其是在 IDEA 的帮助下,只需在键盘上按下 `so` 两个字母就可以调出 `System.out.println()`。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-64dbb12b-8f6b-4ee3-ab5a-60519dd9112f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-64dbb12b-8f6b-4ee3-ab5a-60519dd9112f.png) 在本地环境下,使用 `System.out.println()` 打印日志是没问题的,可以在控制台看到信息。但如果是在生产环境下的话,`System.out.println()` 就变得毫无用处了。 @@ -63,7 +63,7 @@ OFF,最高级别,意味着所有消息都不会输出了。 这个级别是基于 Log4j 的,和 java.util.logging 有所不同,后者提供了更多的日志级别,比如说 SEVERE、FINER、FINEST。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-4919cd20-e524-43a2-8b41-9eab6ac0c1e4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-4919cd20-e524-43a2-8b41-9eab6ac0c1e4.png) ### 03、错误的日志记录方式是如何影响性能的 @@ -86,7 +86,7 @@ if(logger.isDebugEnabled()){ 切记,在生产环境下,一定不要开启 DEBUG 级别的日志,否则程序在大量记录日志的时候会变很慢,还有可能在你不注意的情况下,悄悄地把磁盘空间撑爆。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-fd2149c5-2d0c-4c15-897d-d5fa06cce71f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-fd2149c5-2d0c-4c15-897d-d5fa06cce71f.png) ### 04、为什么选择 Log4j 而不是 java.util.logging @@ -123,7 +123,7 @@ public class JavaUtilLoggingDemo { 程序运行后会在 target 目录下生成一个名叫 javautillog.txt 的文件,内容如下所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-222181d4-04ba-4487-8386-69b8737d2d5c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-222181d4-04ba-4487-8386-69b8737d2d5c.png) 再来看一下 Log4j 的使用方式。 @@ -349,11 +349,11 @@ if(logger.isDebugEnabled()) { 8)不要在日志文件中打印密码、银行账号等敏感信息。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-42d3a052-daeb-450a-a775-a32f983dd688.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-42d3a052-daeb-450a-a775-a32f983dd688.png) ### 06、 总结 打印日志真的是一种艺术活,搞不好会严重影响服务器的性能。最可怕的是,记录了日志,但最后发现屁用没有,那简直是苍了个天啊!尤其是在生产环境下,问题没有记录下来,但重现有一定的随机性,到那时候,真的是叫天天不应,叫地地不灵啊! -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/log4j2.md b/docs/gongju/log4j2.md similarity index 94% rename from docs/src/gongju/log4j2.md rename to docs/gongju/log4j2.md index b46aa04c3e..ac617061e7 100644 --- a/docs/src/gongju/log4j2.md +++ b/docs/gongju/log4j2.md @@ -14,7 +14,7 @@ SLF4J 和 Logback 作为 Log4j 的替代品,在很多方面都做了必要的 上一篇也说了,老板下死命令要我把日志系统切换到 Logback,我顺利交差了,老板很开心,夸我这个打工人很敬业。为了表达对老板的这份感谢,我决定偷偷摸摸地试水一下 Log4j 2,尽管它还不是个成品,可能会会项目带来一定的隐患。但谁让咱是一个敬岗爱业的打工人呢。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-a9461265-7652-4512-9219-6b3e82392415.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-a9461265-7652-4512-9219-6b3e82392415.png) ### 01、Log4j 2 强在哪 @@ -25,7 +25,7 @@ Log4j 2 的异步 Logger 使用的是无锁数据结构,而 Logback 和 Log4j 下图说明了多线程方案中无锁数据结构对吞吐量的影响。 Log4j 2 随着线程数量的扩展而更好地扩展:具有更多线程的应用程序可以记录更多的日志。其他日志记录库由于存在锁竞争的关系,在记录更多线程时,总吞吐量保持恒定或下降。这意味着使用其他日志记录库,每个单独的线程将能够减少日志记录。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-43f0b03d-5c4a-4af3-9e4c-177956246740.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-43f0b03d-5c4a-4af3-9e4c-177956246740.png) 性能方面是 Log4j 2 的最大亮点,至于其他方面的一些优势,比如说下面这些,可以忽略不计,文字有多短就代表它有多不重要。 @@ -86,7 +86,7 @@ Log4j 2 竟然没有在控制台打印“ log4j2”,还抱怨我们没有为 可以在方法中打个断点,然后 debug 一下,你就会看到下图中的内容。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-4ba440d9-c0b6-4ad2-b538-9d303cc99d90.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-4ba440d9-c0b6-4ad2-b538-9d303cc99d90.png) 通过源码,你可以看得到,Log4j 2 会去寻找 4 种类型的配置文件,后缀分别是 properties、yaml、json 和 xml。前缀是 log4j2-test 或者 log4j2。 @@ -259,7 +259,7 @@ for (int i = 1;i < 20; i++) { 再次运行 Demo 类,可以看到根目录下多了 3 个日志文件: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-07af98ca-cf94-427e-adb6-bd935e32a8d0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-07af98ca-cf94-427e-adb6-bd935e32a8d0.png) 结合日志文件名,再来看 RollingFile 的配置,就很容易理解了。 @@ -272,7 +272,7 @@ for (int i = 1;i < 20; i++) { 先来看一下 DefaultRolloverStrategy 的属性: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-32b853ce-8beb-496b-b66f-31b650c257ab.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-32b853ce-8beb-496b-b66f-31b650c257ab.png) 再来看 filePattern 的值 `rolling-%d{yyyy-MM-dd}-%i.log`,其中 `%d{yyyy-MM-dd}` 很好理解,就是年月日;其中 `%i` 是什么意思呢? @@ -304,9 +304,9 @@ for (int i = 1;i < 20; i++) { 运行 Demo 后,可以在 gz 目录下看到以下文件: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-1b04167d-a11f-4447-9062-cb3cdd59aa73.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-1b04167d-a11f-4447-9062-cb3cdd59aa73.png) 到此为止,Log4j 2 的基本使用示例就已经完成了。测试环境搞定,我去问一下老板,要不要在生产环境下使用 Log4j 2。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/gongju/logback.md b/docs/gongju/logback.md new file mode 100644 index 0000000000..e96800491f --- /dev/null +++ b/docs/gongju/logback.md @@ -0,0 +1,414 @@ +--- +title: Logback:Spring Boot内置的日志处理框架 +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 +--- + +就在昨天,老板听我说完 Logback 有多牛逼之后,彻底动心了,对我下了死命令,“这么好的日志系统,你还不赶紧点,把它切换到咱的项目当中!” + +我们项目之前用的 Log4j,在我看来,已经足够用了,毕竟是小公司,性能上的要求没那么苛刻。 + +![](http://cdn.tobebetterjavaer.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 那样加一个适配层。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-6ba1b465-5533-49dd-b875-48a10ba29f8e.png) + +2)Spring Boot 的默认日志框架使用的是 Logback。一旦某款工具库成为了默认选项,那就说明这款工具已经超过了其他竞品。 + +![](http://cdn.tobebetterjavaer.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)): + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-82d78a15-8ae0-4377-a7af-aebd5cda4fda.png) + +也可以通过源码的形式看得到: + +![](http://cdn.tobebetterjavaer.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 会自动导入另外两个依赖: + +![](http://cdn.tobebetterjavaer.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 或多个 `` 元素,其后再跟最多只能存在一个的 `` 元素。 + +![](http://cdn.tobebetterjavaer.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 的情况): + +![](http://cdn.tobebetterjavaer.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 +``` + +粘贴到该网址的文本域: + +![](http://cdn.tobebetterjavaer.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 都在。 + +![](http://cdn.tobebetterjavaer.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。 + +![](http://cdn.tobebetterjavaer.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 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/others.md b/docs/gongju/others.md similarity index 100% rename from docs/src/gongju/others.md rename to docs/gongju/others.md diff --git a/docs/src/gongju/slf4j.md b/docs/gongju/slf4j.md similarity index 86% rename from docs/src/gongju/slf4j.md rename to docs/gongju/slf4j.md index 6dd92aca4b..220072a83e 100644 --- a/docs/src/gongju/slf4j.md +++ b/docs/gongju/slf4j.md @@ -12,11 +12,11 @@ tag: (为什么我把这段文字手敲了下来呢,因为我发现阿里巴巴开发手册上的有语病,瞧下面红色标出的部分) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-94ba034a-c6e6-46e0-bff3-b658bf35945f.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-a7a6e0ae-cbee-428e-8a45-2f5a33243625.png) 那看到这条强制性的规约,我就忍不住想要问:“为什么阿里巴巴开发手册会强制使用 SLF4J 作为 Log4J 的门面担当呢?”究竟这背后藏了什么“不可告人”的秘密? @@ -32,11 +32,11 @@ SLF4J 是 Simple Logging Facade for Java 的缩写(for≈4),也就是简 SLF4J 的作者就是 Log4J 和 Logback 的作者,他的 GitHub 主页长下面这样: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-c72cd63d-b15b-401c-8399-ad0355f1f802.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-cdc9e0fb-71ab-42e7-8024-7e9cfd9b30c3.png) 1996 年初,欧洲安全电子市场项目决定编写自己的跟踪 API,最后该 API 演变成了 Log4j,已经推出就备受宠爱。 @@ -44,17 +44,17 @@ SLF4J 的作者就是 Log4J 和 Logback 的作者,他的 GitHub 主页长下 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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-3044b416-ff14-408f-b933-71993b7ddeee.png) 巨佬动手,丰衣足食,有没有?狠起来连自己的 Log4j 都搭个桥。 @@ -70,13 +70,13 @@ Ceki Gulcu 也觉得 JCL 不好,要不然他也不会在 2005 年自己撸一 假设我们正在开发一套系统,打算用 SLF4J 作为门面,Log4j 作为日志系统,我们在项目中使用了 A 框架,而 A 框架的门面是 JCL,日志系统是 JUL,那就相等于要维护两套日志系统,对吧? -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-e66a78a6-1ef6-42c1-86fa-a4c57b3ef160.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-5e1021d6-6a81-492b-b8d3-f9438014b53b.png) - 使用 jcl-over-slf4j.jar 替换 commons-logging.jar - 引入 jul-to-slf4j.jar @@ -116,7 +116,7 @@ public class Demo { 调试这段代码的过程中你会发现,Log 的实现有四种: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-a1db024d-1b29-47b2-a1f8-70b899d5b7c0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-a1db024d-1b29-47b2-a1f8-70b899d5b7c0.png) 如果没有绑定 Log4j 的话,就会默认选择 Jdk14Logger——它返回的 Logger 对象,正是 java.util.logging.Logger,也就是 JUL。 @@ -197,15 +197,15 @@ private static Log logger = LogFactory.getLog(Demo.class); SLF4J 除了提供这种解决方案,绑定 Log4j 替换 JUL 和 JCL;还提供了绑定 Logback 替换 JUL、JCL、Log4j 的方案: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-d3162919-47e1-4760-beba-7b77cdf42e71.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-e9233a42-d13e-4d7d-9d9e-b049d08303aa.png) ### 03、SLF4J 比 Log4J 强在哪 @@ -282,7 +282,7 @@ public class Log4jSLF4JDemo { 看到了吧,使用占位符要比“+”操作符方便的多。并且此时不再需要 `isDebugEnabled()` 先进行判断,`debug()` 方法会在字符串拼接之前执行。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-5e831353-b2a3-4a39-80e3-47a044009d95.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-5e831353-b2a3-4a39-80e3-47a044009d95.png) 如果只是 Log4J 的话,会先进行字符串拼接,再执行 `debug()` 方法,来看示例代码: @@ -295,13 +295,13 @@ logger.debug(name + ",年纪:" + age + ",是个非常不要脸的程序员 在调试这段代码的时候,你会发现的,如下图所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-4ae3eda7-cd0d-4094-9331-d6070a39c8ea.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-51004734-620a-4c1a-8aa7-6d7f3e1781d6.png) 这是因为如果参数是基本数据类型的话,会先进行自动装箱(`Integer.valueOf()`)。测试代码如下所示: @@ -317,7 +317,7 @@ 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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-aec16f40-7849-4a1d-9507-9119707e6c79.png) 也就是说,如果不 `isDebugEnabled()` 的话,在不是 DEBUG 级别的情况下,会多执行自动装箱和调用其他方法的操作——程序的性能就下降了! @@ -337,4 +337,4 @@ logger.debug("\u6C89\u9ED8\u738B\u4E8C\uFF0C{}\u5C81", Integer.valueOf(18)); -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/gongju/tabby.md b/docs/gongju/tabby.md similarity index 76% rename from docs/src/gongju/tabby.md rename to docs/gongju/tabby.md index 4a9915fd53..7c02af28dc 100644 --- a/docs/src/gongju/tabby.md +++ b/docs/gongju/tabby.md @@ -1,19 +1,17 @@ --- -title: Tabby:一款逼格更高的开源终端工具,GitHub 星标 50+k -shortTitle: Tabby:开源终端工具 +title: Tabby:一款逼格更高的开源终端工具,GitHub 星标 21.4k +shortTitle: Tabby:一款逼格更高的开源终端工具 category: - Java企业级开发 tag: - 辅助工具 -description: Tabby:一款逼格更高的开源终端工具,GitHub 星标 50+k +description: Tabby:一款逼格更高的开源终端工具,GitHub 星标 21.4k head: - - meta - name: keywords content: 辅助工具,GitHub,终端,Tabby,tabby 终端,tabby 教程,Java企业级开发 --- -大家好,我是二哥呀。 - 作为一名 Java 后端开发,日常工作中免不了要和 Linux 服务器打交道,因为生产环境基本上都是部署在 Linux 环境下的。以前呢,我会选择 Xshell 来作为终端进行远程操作。 随着付费版本的出现,尤其是 Xshell 把 FTP 分离出去后,上传下载文件的话还需要单独装一下 Xftp,这显然没有之前集成在一起方便😖。 @@ -26,9 +24,9 @@ head: 答案是有的,它就是 **Tabby**! -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-01.png) -GitHub 上已经有 52.8k 的 star 了,这说明 Tabby 非常的受欢迎: +GitHub 上已经有 21.4k 的 star 了,这说明 Tabby 非常的受欢迎: >[https://github.com/eugeny/tabby](https://github.com/eugeny/tabby) @@ -38,9 +36,9 @@ Tabby 是一个高度可定制化的 跨平台的终端工具,支持 Windows ## 一、安装 Tabby -直接到官网 [tabby.sh](https://tabby.sh/) 点击「download」按钮就可以跳转到下载页面,最新的 release 版本是 1.0.205。 +直接到官网 [tabby.sh](https://tabby.sh/) 点击「download」按钮就可以跳转到下载页面,最新的 release 版本是 1.0.164。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-02.png) Linux 和 Windows 的比较好选,macOS 分为两个版本,一个是 arm64,一个是 x86-64,什么意思呢? @@ -60,59 +58,61 @@ Apple M1 是苹果公司的第一款基于ARM架构的自研处理器单片系 按照提示,一步步安装就 OK 了。完成后打开,这界面还是非常炫酷的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-03.png) +![](http://cdn.tobebetterjavaer.com/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.paicoding.com/tobebetterjavaer/images/gongju/tabby-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-04.png) 填写服务器的 IP 地址和密码,然后点击「save」。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-05.png) 之后点击「运行」按钮,就可以进入到终端页面了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-06.png) 好了,现在可以对服务器进行操作了,执行下 top 命令可以查看服务器上正在运行的进程信息。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-07.png) ## 三、SFTP 传输文件 Tabby 集成了 SFTP,所以上传下载文件就变得非常的简单。只需要点击一下「SFTP」图标就可以打开文件传输窗口。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-08.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-08.png) 上传的时候支持拖拽,完成后会弹出文件传输成功的提示消息。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-09.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-09.png) 下载的时候点击要下载的文件,然后会弹出存储对话框,选择对应的文件夹,以及修改对应的文件名点击「存储」就可以了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-10.png) ## 四、配置 Tabby 「Settings」 的面板下有一个「Appearance」的菜单,可以对 Tabby 的外观进行设置,比如说调整字体,比如说自定义样式。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-11.png) 「Appearance」的菜单可以对 Tabby 的配色方案进行修改,里面的主题非常多,不过我感觉默认的就挺不错,毕竟是官方推荐的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-12.png)  「Plugins」 菜单中还有不少插件可供扩展。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-13.png) +![](http://cdn.tobebetterjavaer.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 容器 @@ -122,25 +122,25 @@ Tabby 集成了 SFTP,所以上传下载文件就变得非常的简单。只需 这里重点说一下「sync config」 这个插件,可以将配置同步到Github或者Gitee的插件。点击「Get」就可以安装,之后会提示你重启生效。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-14.png) 生效后点击「Sync Config」菜单,就可以看到配置项了,类型可以选择 GitHub、Gitee、GitLab。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-15.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-15.png) 这里以 Gitee 为例,进入个人 Gitee 主页,左侧菜单中选择「私人令牌」,然后点击「生成新令牌」。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-16.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-16.png) 提交后会生成 token,复制到 Tabby 的 Token 输入框中,然后点击「Upload config」,就可以看到配置信息同步成功了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-17.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-17.png)  「Window」 菜单中可以对当前窗口进行设置,比如说改变窗口的主题为 Paper,改变 tab 的位置到底部等等。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-18.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-18.png) ## 五、总结 @@ -148,12 +148,9 @@ SSH 连接和 SFTP 传输恐怕是我们操作 Linux 服务器最常用的两个 Windows 用户习惯用 Xshell,macOS 用户习惯用 iTerm2,但这两款工具都没办法跨平台,多平台操作的用户就可以选择 Tabby 来体验一下,真心不错。 -![](https://cdn.paicoding.com/stutymore/tabby-20231219193213.png) - - Tabby 的学习资料还比较少,所以希望二哥的这篇文章能给有需要的小伙伴提供一点点的帮助和启发。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/gongju/warp.md b/docs/gongju/warp.md new file mode 100644 index 0000000000..546a28fe18 --- /dev/null +++ b/docs/gongju/warp.md @@ -0,0 +1,181 @@ +--- +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://tobebetterjavaer.com/gongju/tabby.html) 吗?是时候喜新厌旧了。 + + +![](http://cdn.tobebetterjavaer.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 了。 + +![](http://cdn.tobebetterjavaer.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」就可以下载最新版了。下载完成后,双击安装包就可以安装了。完成后打开,界面还是非常清爽的。 + +![](http://cdn.tobebetterjavaer.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/) + +如果顺利登录,会跳转到这个页面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-84c0b513-57f3-4ab4-8c77-508c10c923c5.png) + +填写一些 Warp 的调查信息后,就会跳转到 Warp 的初始界面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-304639e9-7554-45b4-a199-e7c0c3b40c33.png) + +>需要注意的是,Warp 目前仅支持 macOS 版,Linux 和 Windows 用户还需要等待一段时间。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-a4d43a50-0ad9-4f91-ad4e-c1c0788bb580.png) + +其实 macOS 版也是刚刚公测,我这份攻略绝壁是热乎乎的。想要第一时间关注 Warp 版本信息的话,可以戳下图中提到的链接填写自己的邮箱。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-f622d505-b136-4b9d-95c5-a6872e1423e1.png) + +## 二、使用 Warp + +Warp 解决的第一个痛点,就是减少配置、方便输入、优化输出,并且增加常用命令的自动提示。 + +**1)智能提示** + +普通的终端在你键入 tab 的时候,是这样提示的,就是简单地帮你罗列下。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-078017c6-a872-466a-8aa2-f202c9371493.png) + +而 Warp 就非常的时髦,会给你滚动可选的列表形式展示出来。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-4e205289-d8c4-49a9-90ba-08aef8beb627.png) + +Warp 的智能提示也更加“智能化”,它会猜测你下一步的命令到底输入什么。 + +比如说我的工作目录下有一个 README.md 的文件,那当我输入 `echo '沉默王二' >>`的时候它会把 `README.md` 提示在后面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-8948ab59-3ce8-4b04-80a5-ecd7663e1034.png) + +**2)智能记忆** + +Warp 会记录上一次执行的命令,在顶部会有一个提示的按钮,当你点击的时候,它会自动滚动到上一个命令执行的位置。 + +点击「clear」之前。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-6055bfaa-a146-4cf8-a6f4-aa493dbfa60b.png) + +点击「clear」之后。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-181dff97-bd6f-4c41-94c8-8e9ac5567460.png) + +**3)区域选择** + +传统的终端,在复制区域命令和输出结果的时候需要全部手动选择,而 Warp 是可以点选的,之后可以通过右键菜单进行复制粘贴(可以选择只复制命令或者输出,也可以都选),非常方便。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-23c0a936-2371-4cf4-acf1-555bceecac44.png) + +**4)历史命令** + +传统的终端在通过 up-down 键选择历史命令的时候,一次只能提示一个命令。而 Warp 会把历史命令做成一个滚动的可以选择的列表。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-da43dca3-d8d1-43ad-9308-f8ec1c2b871b.png) + + +**5)命令导航** + +同时按下 Ctrl+Shift+R 可以打开命令导航,Warp 集成了很多工具的命令导航。比如说我们要执行 `git reset` 命令,那么到底格式什么,应该怎么执行,Warp 都提示的非常到位。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-b9a4fd3f-24b2-4f6a-8a70-58fa1b313df3.png) + +这让我想起了 macOS 的效率工具 Alfred,可以搜索任何你想要的命令。 + +**6)AI 植入** + +Warp 还提供了 AI 智能搜索,快捷键可以在 setting→keyboard shortcuts 中找得到,键入 AI 关键字即可。 + +可调整为自己喜欢的快捷键。我目前设置的是 `Ctrl+shift+>`。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-04300bc5-5d0d-494b-955c-1d270133227a.png) + +比如说我问它“how many lines were changed in the last 2 commits?” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-871e51e9-c2ac-4ecb-bfcb-ff339f05bd61.png) + +Warp 解决的第二个痛点是增加协作功能。不过由于我目前没有邀请其他用户参与,还无法使用共享功能,后面有小伙伴体验的话,可以通过我分享的链接下载试一波。 + +>https://app.warp.dev/referral/25KR3Y + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-d8952a84-a0a7-4c3d-b237-87cdc997bb4c.png) + +## 三、配置 Warp + +输入 Command+P 快捷键可以打开 Warp 的命令面板。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-369d16f6-f897-4c9f-bbea-631d561e145b.png) + +键入 `sett` 关键字就可以打开配置页。 + +比如说在「Appearance」选项卡里可以设置 Warp 的主题、字体,以及紧凑型模式。 + +大概有十多种主题可选,比如说这个女生非常喜欢的粉色系。 + +![](http://cdn.tobebetterjavaer.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」就可以了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-aa785da8-bb39-4851-97f5-b7f8baaccf34.png) + + +## 四、总结 + +最后总结一波吧。 + +这波着实属于尝鲜了,市面上应该还木有 Warp 终端的普及安利文章,我这期应该属于大姑娘坐花轿———头一回。 + +害,登录折腾了好久,原因我就不多说了,小伙伴们自行体会哈。反正我是没被劝退。 + +幸好是没放弃,所以才体验到了 Warp 的强大之处,真的是改变了我对终端 terminal 的认知——太特喵的炫酷了! + +这个过程就有点陶渊明《桃花源记》里那句“初极狭,复行数十步,豁然开朗”的赶脚。 + +喜欢的小伙伴一定要尝试一把,你会来感谢我的。好了,这期就先聊到这吧,毕竟 Warp 刚公测,后面有机会再来给大家详细地说。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/gongju/windterm.md b/docs/gongju/windterm.md new file mode 100644 index 0000000000..169a844bd9 --- /dev/null +++ b/docs/gongju/windterm.md @@ -0,0 +1,195 @@ +--- +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://tobebetterjavaer.com/gongju/tabby.html)、[Warp](https://tobebetterjavaer.com/gongju/warp.html) 后,今天再来给大家推荐一款终端神器——WindTerm,完全开源,在 GitHub 上已经收获 6.6k 的 star。 + +>[https://github.com/kingToolbox/WindTerm](https://github.com/kingToolbox/WindTerm) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-5220349e-fb8b-41c8-94c7-9d37b0eeaa82.png) + +作者还拿 WindTerm 和 Putty、xterm、Windows Terminal + ssh.exe、iterm2、rxvt、Gnome等等做了一个性能对比,结果其他终端均被吊打的不成样子,真正的**杀人诛心** + +哈哈哈哈哈哈哈哈哈哈 + + +![](http://cdn.tobebetterjavaer.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 左右的安装包,真的是良心。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-f7abe795-d43b-4f53-93a5-e59241d45930.png) + +安装完成后,打开的界面和传统的终端不太一样,WindTerm 更像 IDE 的布局,左边是资源管理器+文件管理器,中间会默认打开一个 zsh 的终端窗口,右边是会话窗口+历史命令窗口,底部是发送窗口 + Shell 窗口。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-28d20f75-c5d0-47bf-96db-a2f470f03c42.png) + +## 使用 WindTerm + +### SSH + +使用终端最重要的一个场景就是 SSH,连接远程服务器,我这里有一个 1G 内存的轻量级云服务器,我们来连接它体验一下。 + +点击新建会话按钮开始 SSH 连接。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-21565ed0-90d8-466f-b505-d1d2f58388be.png) + +添加主机名,点击「连接」开始进行远程链接。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-fec8d31e-aa33-4d5e-b0c0-4c7f09ea208b.png) + +紧接着输入用户名和密码,我们关掉一些没必要的窗口,让整个界面更加清爽一些。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-aae1b222-c6da-4285-8efe-e87e5cc66702.png) + +如果感觉字体比较小的话,可以直接按住**「command+」**两个组合键放大字体。 + +WindTerm 给我一个非常直观的操作是,它提供了一个折叠的功能,点击-号折叠,点击+号展开。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-e3415e9c-d002-4492-af9d-83b02e87c7d8.png) + +还有一个就是智能提示,非常到位,响应速度很快。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-a34d2157-3a5d-4afa-a5c4-2cc323244c4f.png) + + +### SFTP + +除了 SSH,还有一个重要的场景就是上传文件,我们知道,Xshell 是直接将 FTP 分离了出去,我总觉得这个产品分割设计很脑残,放在一起挺好的。 + +WindTerm 是放在一起的,直接打开文件文件管理器,选择文件上传还是直接拖拽,都非常便利。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-e23d187e-9d67-4e6a-9b22-e9a3a0e459a5.png) + +文件上传完成后会有一个进度条提示。 + +如果想直接在 SSH 窗口中上传文件的话,就需要安装lrzsz。如果没安装的话,会提示错误❎。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-3eb27b2e-ba98-44f2-8ec3-4ec86e9f62d1.png) + +因为我的远程服务器是 CentOS,所以执行 `yum install lrzsz`就可以直接安装了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-c6f757b9-a6e0-4bd9-beef-9ce501cbdf41.png) + +安装完成后就可以直接在 SSH 上传文件了,和其他终端不同的是,WindTerm 会有进度条提示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-ee7b8acf-6e39-42d7-ab92-bf8e24243c38.png) + +WindTerm 还提供了高速传输模式,上传下载速度更快。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-f2315d1b-d6c9-4eff-8470-2088cff6cd05.png) + + +搞定 SSH 和 SFTP,一个终端的基础功能就全具备了,这也是我们最常用的两个场景。WindTerm 在这两方面都做的不错。 + +### 自动补全 + +WindTerm 的自动补全功能还是非常强大的,只需要在行首键入 `!` 就可以调出历史命令,然后使用向下的箭头选择历史命令就 OK 了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-838a4711-4e09-46d7-a0d5-99e302889d27.png) + +WindTerm 能够自动补全的命令非常全面,支持: + +- Linux Shell 命令。 +- MacOS Shell 命令。 +- Windows Cmd 命令。 +- PowerShell 命令。 +- 任何命令行程序的命令,例如 Git + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-62b525bf-c4c6-4e3a-b313-884c945809e3.png) + + + +## 配置 WindTerm + +### 如何重置锁屏密码 + +不过有点小尴尬😓的是,WindTerm有自动锁屏的功能,过段时间(默认 30 分钟)没有操作,就会自动锁屏。然而,我之前并没有设置过锁屏密码,这就好像我自己的门我自己锁了,却没有钥匙🔑。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-17017bd4-4da6-41f5-bf48-2b056dbfe258.png) + +虽然提供了更改主密码的功能,但我就不知道初始密码是什么,就更尴尬了。 + +怎么办? + +遇事不决问 issue:**如何重置锁屏密码**! + + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-759ffc3b-0119-4a2b-bce5-68c7f8612b31.png) + + +果然已经有小伙伴提出了这个问题,我们顺藤摸瓜就可以搞定了,找到 user.config 文件。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-f4a8b033-5d77-4361-935e-66c210e67690.png) + +干掉 application.fingerprint 和 application.masterPassword。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-4cd13932-07a3-4696-9752-265ed76c4463.png) + +再找到 .wind/profiles/default.v10/terminal/user.sessions 文件删除 session.autoLogin 就可以将主密码设置为空字符串了,之后再来修改主密码,就 OK 了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-21423050-3de4-4afe-9e4a-0b073c4f6504.png) + +### 更换主题 + +WindTerm 支持三种主题的切换,亮白模式、暗黑模式、黑白相间模式。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-6b123b12-903e-4040-86e6-95162df4aa09.png) + +我们来切换到亮白模式体验一下,还不错。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-a18114a4-17d5-4f83-b96d-f9007d67e560.png) + +### 自动复制 + +只需要在设置中,找到文本一栏,勾选「自动复制选定内容」就可以了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-fd333685-e76e-434c-832e-2f2b594dfd35.png) + +选中内容,然后就直接复制了。 + +## 总结 + +总的来说,WindTerm 的体验不错,除了我上面提到的这些基础功能外,像分屏啊,转接端口啊,并且在 Windows 下的体验要比 macOS 操作系统下更酷一些。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/windterm-201419f5-0097-4fe2-b24f-3d35c25c18d0.png) + + +作者把两者的使用技巧全部分享到了下面这个网址上,小伙伴们可以去解锁一下。 + +>[https://kingtoolbox.github.io/](https://kingtoolbox.github.io/) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/home.md b/docs/home.md new file mode 100644 index 0000000000..220ccb6bee --- /dev/null +++ b/docs/home.md @@ -0,0 +1,514 @@ +--- +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教程,从入门到进阶 +--- + +>[二哥的编程星球](https://tobebetterjavaer.com/zhishixingqiu/)已经有 **800 多名** 小伙伴加入了,如果你也需要一个良好的学习氛围,[戳链接](https://tobebetterjavaer.com/zhishixingqiu/)加入我们吧!这是一个 Java 学习指南 + 编程实战的私密圈子,你可以向二哥提问、帮你制定学习计划、跟着二哥一起做项目、刷力扣,冲冲冲。 + +::: center +

+ + Java 程序员进阶之路 + +

+ +

+ + + + 无套路下载 + Java程序员进阶之路 +

+::: + + +## 为什么会有这个开源知识库 + + +知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 Java 程序员进阶之路😄。 + + 知识库旨在为学习 Java 的小伙伴提供一系列: + - **优质的原创 Java 教程** + - **全面清晰的 Java 学习路线** + - **免费但靠谱的 Java 学习资料** + - **精选的 Java 岗求职面试指南** + - **Java 企业级开发所需的必备技术** + +赠人玫瑰手有余香。知识库会持续保持**更新**,欢迎收藏品鉴! + +**转载须知** :以下所有文章如非文首说明为转载皆为我(沉默王二)的原创,转载在文首注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! + +推荐你通过在线阅读网站进行阅读,体验更好,速度更快! + +- [Java 程序员进阶之路在线阅读网站](https://tobebetterjavaer.com/) +- [Java 程序员进阶之路 GitHub 仓库](https://github.com/itwanger/toBeBetterJavaer) +- [Java 程序员进阶之路 Gitee 仓库](https://gitee.com/itwanger/toBeBetterJavaer) + +亿点点小请求,建议戳[这个链接🔗](https://github.com/itwanger/toBeBetterJavaer)给本仓库点个 star,满足一下我的虚荣心,内容质量也绝对值得你一个 star。我还在继续创作,给我一点继续更新的动力,笔芯。 + +## 知识库地图 + + +知识库收录的核心内容就全在这里面了,大类分为 Java 核心、Java 企业级开发、数据库、计算机基础、求职面试、学习资源、程序人生,几乎你需要的这里都有。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/tobebetterjavaer-map.png) + +## 学习路线 + +除了 Java 学习路线,还有 C语言、C++、Python、Go 语言、操作系统、前端、数据结构与算法、蓝桥杯、大数据、Android、MySQL等硬核学习路线,欢迎收藏品鉴! + +* [Java学习路线一条龙版(建议收藏🔥)](xuexiluxian/java/yitiaolong.md) +* [Java并发编程学习路线(建议收藏🔥)](xuexiluxian/java/thread.md) +* [Java虚拟机学习路线(建议收藏🔥)](xuexiluxian/java/jvm.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) +* [MySQL 学习路线(建议收藏🔥)](xuexiluxian/mysql.md) + +## 面渣逆袭 + +**面试前必读系列**!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ 等等。 + +- [面渣逆袭(Java 基础篇八股文面试题)必看👍](sidebar/sanfene/javase.md) +- [面渣逆袭(Java 集合框架篇八股文面试题)必看👍](sidebar/sanfene/collection.md) +- [面渣逆袭(Java 并发编程篇八股文面试题)必看👍](sidebar/sanfene/javathread.md) +- [面渣逆袭(Java 虚拟机篇八股文面试题)必看👍](sidebar/sanfene/jvm.md) +- [面渣逆袭(Spring八股文面试题)必看👍](sidebar/sanfene/spring.md) +- [面渣逆袭(Redis八股文面试题)必看👍](sidebar/sanfene/redis.md) +- [面渣逆袭(MyBatis八股文面试题)必看👍](sidebar/sanfene/mybatis.md) +- [面渣逆袭(MySQL八股文面试题)必看👍](sidebar/sanfene/mysql.md) +- [面渣逆袭(操作系统八股文面试题)必看👍](sidebar/sanfene/os.md) +- [面渣逆袭(计算机网络八股文面试题)必看👍](sidebar/sanfene/network.md) +- [面渣逆袭(RocketMQ八股文面试题)必看👍](sidebar/sanfene/rocketmq.md) + + +## Java 核心 + +**Java 核心非常重要**!我将其分成了Java 基础篇(包括基础语法、面向对象、集合框架、异常处理、Java IO 等)、Java 并发篇和 Java 虚拟机篇。 + +### Java概述 + +- [Java简介,什么是 Java?](overview/what-is-java.md) +- [Java发展简史](overview/java-history.md) +- [Java的特点,Java能做什么](overview/java-can-do-what.md) +- [安装Java开发工具包JDK](overview/jdk-install-config.md) +- [安装集成开发环境Intellij IDEA](overview/IDEA-install-config.md) +- [编写第一个Java程序:Hello World](overview/hello-world.md) + + +### Java基础语法 + +- [Java中常用的48个关键字 & 2个保留字](basic-extra-meal/48-keywords.md) +- [Java命名规范,告别编码 5 分钟,命名 2 小时](basic-extra-meal/java-naming.md) +- [Java注释:单行、多行和文档注释](basic-grammar/javadoc.md) +- [Java 中的数据类型(8 种基本数据类型和引用数据类型)](basic-grammar/basic-data-type.md) +- [Java 数据类型转换(强制类型转换+自动类型转换)](basic-grammar/type-cast.md) +- [Java流程控制语句](basic-grammar/flow-control.md) +- [Java运算符](basic-grammar/operator.md) + +### 数组&字符串 + +- [浅入浅出Java数组](array/array.md) +- [如何优雅地打印Java数组?](array/print.md) +- [为什么String是不可变的?](string/immutable.md) +- [深入理解Java字符串常量池](string/constant-pool.md) +- [深入解析String.intern()方法](string/intern.md) +- [Java如何判断两个字符串是否相等?](string/equals.md) +- [最优雅的Java字符串拼接是哪种方式?](string/join.md) +- [如何在Java中优雅地分割String字符串?](string/split.md) + +### 面向对象编程 + +- [怎么理解Java中的类和对象?](oo/object-class.md) +- [Java 包,优雅地解决类名冲突](oo/package.md) +- [Java中的变量:局部变量、成员变量、静态变量、常量](oo/var.md) +- [Java中的方法:实例方法、静态方法、抽象方法](oo/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/abstract-vs-interface.md) +- [Java内部类详解:成员内部类、局部内部类、匿名内部类、静态内部类](oo/inner-class.md) +- [Java中this和super的用法总结](oo/this-super.md) +- [Java 方法重写 Override 和方法重载 Overload 的区别,一下子就明白了](basic-extra-meal/override-overload.md) +- [一文彻底搞懂 Java static 关键字:静态变量、静态方法、静态代码块、静态内部类](oo/static.md) +- [一文彻底搞懂 Java final 关键字](oo/final.md) +- [这次彻底搞清楚了 Java 的三大特征之一:封装](oo/encapsulation.md) +- [一万字彻底搞懂 Java 继承(三大特征之一)](oo/extends-bigsai.md) +- [几句话,直观解释清楚 Java 多态(三大特征之一)](oo/polymorphism.md) + +### 集合框架(容器) + +- [Java 集合框架(容器)体系结构](collection/gailan.md) +- [衡量Java集合框架执行效率的重要标准:时间复杂度](collection/time-complexity.md) +- [Java ArrayList详解(附源码分析)](collection/arraylist.md) +- [Java LinkedList详解(附源码分析)](collection/linkedlist.md) +- [面试官问我Java中ArrayList和LinkedList的区别,我和他扯了半小时](collection/list-war-2.md) +- [海康威视一面:Java中Iterator和Iterable有什么区别?](collection/iterator-iterable.md) +- [为什么阿里巴巴强制不要在foreach里执行删除操作?还不是因为fail-fast](collection/fail-fast.md) +- [Java HashMap详解(附源码分析)](collection/hashmap.md) +- [Java LinkedHashMap详解(附源码分析)](collection/linkedhashmap.md) +- [Java TreeMap详解(附源码分析)](collection/treemap.md) +- [详解 Java 中的堆和队列(Stack and Queue 附源码分析)](collection/arraydeque.md) +- [详解 Java 中的优先级队列(PriorityQueue 附源码分析)](collection/PriorityQueue.md) +- [Java WeakHashMap详解(附源码分析)](collection/WeakHashMap.md) + + +### Java输入输出 + +- [看完这篇,Java IO 不再混乱!](io/shangtou.md) +- [详解 File、Path、Paths、Files 四个类,Java操作文件不再难](io/file-path.md) +- [文件的世界,一切皆字节流(Stream)](io/stream.md) +- [Java字符流Reader和Writer的故事](io/reader-writer.md) +- [Java缓冲流(Buffered):读写速度有了质的飞升](io/buffer.md) +- [Java转换流,解决字符与字节之间编码、解码的乱码问题](io/char-byte.md) +- [Java序列化流,字节和对象之间的序列化和反序列化](io/serialize.md) +- [Java Serializable:明明就一个空的接口嘛](io/Serializbale.md) +- [招银面试官:说说Java transient关键字](io/transient.md) +- [Java打印流:PrintStream & PrintWriter](io/print.md) + + +### 异常处理 + +- [一文彻底搞懂Java异常处理,YYDS](exception/gailan.md) +- [深入理解 Java 中的 try-with-resouces 语法糖](exception/try-with-resouces.md) +- [Java异常处理的20个最佳实践](exception/shijian.md) +- [Java空指针NullPointerException的传说](exception/npe.md) + +### 常用工具类 + +- [Java Arrays:专为数组而生的工具类](common-tool/arrays.md) +- [Java Collections:专为集合框架而生的工具类](common-tool/collections.md) +- [Hutool:国产良心工具包,让你的Java变得更甜](common-tool/hutool.md) +- [Guava:Google开源的工具库,太强大了](common-tool/guava.md) +- [这10个工具类,让我的开发效率提升了50%](common-tool/utils.md) + +### Java新特性 + +- [Java 8 Stream流详细用法](java8/stream.md) +- [Java 8 Optional最佳指南](java8/optional.md) +- [深入浅出Java 8 Lambda表达式](java8/Lambda.md) + +### Java重要知识点 + +- [彻底弄懂Java中的Unicode和UTF-8编码](basic-extra-meal/java-unicode.md) +- [Java中new Integer与Integer.valueOf的区别](basic-extra-meal/int-cache.md) +- [深入剖析Java中的拆箱和装箱](basic-extra-meal/box.md) +- [一文彻底讲明白的Java中的浅拷贝与深拷贝](basic-extra-meal/deep-copy.md) +- [深入理解Java中的hashCode方法](basic-extra-meal/hashcode.md) +- [为什么重写equals方法的时候必须要重写hashCode方法?](basic-extra-meal/equals-hashcode.md) +- [Java重写(Overriding)时应当遵守的11条规则](basic-extra-meal/Overriding.md) +- [Java到底是值传递还是引用传递?](basic-extra-meal/pass-by-value.md) +- [详解Java中Comparable和Comparator接口的区别](basic-extra-meal/comparable-omparator.md) +- [jdk9为什么要将String的底层实现由char数组改成了byte数组?](basic-extra-meal/jdk9-char-byte-string.md) +- [为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)?](basic-extra-meal/jdk-while-for-wuxian-xunhuan.md) +- [Java 中,先有Class还是先有Object?](basic-extra-meal/class-object.md) +- [详解Java中instanceof关键字的用法](oo/instanceof.md) +- [instanceof关键字是如何实现的?](basic-extra-meal/instanceof-jvm.md) +- [深入理解Java中的不可变对象](basic-extra-meal/immutable.md) +- [Java中可变参数的使用](basic-extra-meal/varables.md) +- [深入理解Java中的泛型](basic-extra-meal/generic.md) +- [Java不能实现真正泛型的原因是什么?](basic-extra-meal/true-generic.md) +- [深入理解Java中的注解](basic-extra-meal/annotation.md) +- [新来个技术总监,彻底把 Java 枚举(enum)讲清楚了](basic-extra-meal/enum.md) +- [大白话说清楚Java反射:入门、使用、原理](basic-extra-meal/fanshe.md) + +### Java网络编程 + +- [Java Socket:飞鸽传书的网络套接字](socket/socket.md) +- [牛逼,用Java Socket手撸了一个HTTP服务器](socket/http.md) + +### Java NIO + +- [为什么我们要使用 Java NIO?](nio/why.md) +- [Java NIO 快速入门(buffer缓冲区、Channel管道、Selector选择器)](nio/rumen.md) +- [一文彻底理解Java IO模型(阻塞IO非阻塞IO/IO多路复用)](nio/moxing.md) +- [使用Java NIO完成网络通信](nio/network-connect.md) +- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](nio/BIONIOAIO.md) + + +### Java并发编程 + + +- [室友打了一把王者就学会了创建Java线程的3种方式](thread/wangzhe-thread.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) +- [Java并发编程volatile关键字解析](thread/volatile.md) +- [Java中的synchronized锁的到底是什么?](thread/synchronized.md) +- [Java实现CAS的原理](thread/cas.md) +- [Java并发AQS详解](thread/aqs.md) +- [大致了解下Java的锁接口和锁](thread/lock.md) +- [公司空降一个美团大佬,彻底把Java中的锁”讲清楚了](thread/suo.md) +- [Java 15 终于把难搞的偏向锁移除了](thread/pianxiangsuo.md) +- [深入理解Java并发重入锁ReentrantLock](thread/reentrantLock.md) +- [深入理解Java并发读写锁ReentrantReadWriteLock](thread/ReentrantReadWriteLock.md) +- [深入理解Java并发线程协作类Condition](thread/condition.md) +- [深入理解Java并发线程线程阻塞唤醒类LockSupport](thread/LockSupport.md) +- [简单聊聊Java的并发集合容器](thread/map.md) +- [吊打Java并发面试官之ConcurrentHashMap](thread/ConcurrentHashMap.md) +- [吊打Java并发面试官之ConcurrentLinkedQueue](thread/ConcurrentLinkedQueue.md) +- [吊打Java并发面试官之CopyOnWriteArrayList](thread/CopyOnWriteArrayList.md) +- [吊打Java并发面试官之ThreadLocal](thread/ThreadLocal.md) +- [吊打Java并发面试官之BlockingQueue](thread/BlockingQueue.md) +- [面试必备:Java线程池](thread/pool.md) +- [深入剖析Java计划任务ScheduledThreadPoolExecutor](thread/ScheduledThreadPoolExecutor.md) +- [Java atomic包中的原子操作类总结](thread/atomic.md) +- [Java并发编程通信工具类CountDownLatch等一网打尽](thread/CountDownLatch.md) +- [深入理解Java并发编程之Fork/Join框架](thread/fork-join.md) +- [从根上理解生产者-消费者模式](thread/shengchanzhe-xiaofeizhe.md) + + +### Java虚拟机 + +- [什么是JVM?](jvm/what-is-jvm.md) +- [JVM到底是如何运行Java代码的?](jvm/how-run-java-code.md) +- [我竟然不再抗拒Java的类加载机制了](jvm/class-load.md) +- [详解Java的类文件(class文件)结构](jvm/class-file-jiegou.md) +- [从javap的角度轻松看懂字节码](jvm/bytecode.md) +- [JVM字节码指令详解](jvm/zijiema-zhiling.md) +- [虚拟机是如何执行字节码指令的?](jvm/how-jvm-run-zijiema-zhiling.md) +- [HSDB(Hotspot Debugger)从入门到实战](jvm/hsdb.md) +- [史上最通俗易懂的ASM教程](jvm/asm.md) +- [自己编译JDK](jvm/compile-jdk.md) +- [深入理解JVM的内存结构](jvm/neicun-jiegou.md) +- [Java 创建的对象到底放在哪?](jvm/whereis-the-object.md) +- [咱们从头到尾说一次Java垃圾回收](jvm/gc.md) +- [图解Java的垃圾回收机制](jvm/tujie-gc.md) +- [Java中9种常见的CMS GC问题分析与解决](jvm/meituan-9-gc.md) +- [Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析)](jvm/problem-tools.md) +- [Java即时编译(JIT)器原理解析及实践](jvm/jit.md) +- [一次内存溢出排查优化实战](jvm/oom.md) +- [一次生产CPU 100% 排查优化实践](jvm/cpu-percent-100.md) +- [JVM 核心知识点总结](jvm/zongjie.md) + +## Java企业级开发 + + - **到底能不能成为一名合格的 Java 程序员,从理论走向实战?Java 企业级开发这部分内容就是一个分水岭**! + - 纸上得来终觉浅,须知此事要躬行。 + + +### 开发工具 + +- [终于把项目构建神器Maven捋清楚了~](maven/maven.md) +- [我在工作中是如何使用Git的](git/git-qiyuan.md) +- [5分钟带你深入浅出搞懂Nginx](nginx/nginx.md) + +### IDE/编辑器 + +- [4个高级的 IntelliJ IDEA 调试技巧](ide/4-debug-skill.md) +- [在 IDEA 里下个五子棋不过分吧?](ide/xechat.md) + +### Spring + +- [Spring AOP扫盲](springboot/aop-log.md) +- [Spring IoC扫盲](springboot/ioc.md) + +### SpringBoot + +- [一分钟快速搭建Spring Boot项目](springboot/initializr.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和Redis的数据一致性?](mysql/redis-shuju-yizhixing.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) +- [12 道 Redis 精选面试题👍](interview/redis-12.md) +- [40 道 Nginx 精选面试题👍](interview/nginx-40.md) +- [60 道 MySQL 精选面试题👍](interview/mysql-60.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/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/ide/4-debug-skill.md b/docs/ide/4-debug-skill.md new file mode 100644 index 0000000000..122f33c9c3 --- /dev/null +++ b/docs/ide/4-debug-skill.md @@ -0,0 +1,152 @@ +--- +title: 4个高级的 IntelliJ IDEA 调试技巧 +shortTitle: IDEA 调试技巧 +category: + - Java企业级开发 +tag: + - Intellij IDEA + - IDEA + - Intellij IDEA 调试技巧 + - IDEA 调试技巧 +description: 几个我日常工作以及阅读源码必备的 IntelliJ IDEA 高级调试技巧,分分钟要起飞的节奏。 +head: + - - meta + - name: keywords + content: Intellij IDEA,IDEA,Intellij IDEA 调试技巧,IDEA 调试技巧 + - name: description + content: 几个我日常工作以及阅读源码必备的 IntelliJ 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 + 鼠标左键`,就会弹出下面的内容 + +![](http://cdn.tobebetterjavaer.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](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-72c23537-3f66-4283-b939-a265b7628a1a.gif) + +如果修改字段值的方法比较多,也可以在 `Condition` 的地方定义断点进入条件, 有了这个功能的加成,相信你阅读源码会顺畅许多 + +## 异常断点 + +除了阅读源码,一定是遇到了异常我们才开始调试代码,代码在抛出异常之后会自动停止,但是我们希望: + +> 代码停在抛出异常之前,方便我们查看当时的变量信息 + +这时我们就用到了 `Exception Breakpoints`, 当抛出异常时,在 catch 的地方打上断点,可以通过下图的几个位置获取栈顶异常类型,比如这里的 `NumberFormatException` + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-c4c511af-b00d-458b-a4a1-97d1fe1e84b8.jpg) + +知道异常类型后,就可以按照如下步骤添加异常断点了: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-4c35cab7-83d2-45b4-8a27-ebeceb41ce08.jpg) + +然后在弹框中选择 NumberFormatException + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-a98e7885-1e84-4c38-8de1-ae04d3013176.gif) + +重新以 Debug 模式运行程序: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-498ad99d-a15d-4a4e-a01b-b0c11cf8f72e.gif) + +程序「一路绿灯式」定位到抛出异常的位置,同时指出当时的变量信息,三个字:稳,准,狠,还有谁? + +## 方法断点 + +当阅读源码时,比如 Spring,一个接口的方法可能被多个子类实现,当运行时,需要查看调用栈逐步定位实现类,IDEA 同样支持在接口方法上添加断点(快捷键 `cmd+F8`/`ctrl+F8`): + +1. 鼠标左键在方法处点击断点(♦️形状) +2. 断点上鼠标右键 + +勾选上绿色框线上的内容,同样可以自定义跳转条件 Condition + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-b81dc459-5a9c-4e0e-b24e-350943299eda.jpg) + +当以 Debug 模式运行程序的时候,会自动进入实现类的方法(注意断点形状): + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-edbc1de2-4dd6-49a3-9a6a-5948d19aabee.jpg) + +看到这你应该想到常见的 Runnable 接口中的 run 方法了,同样是有作用的,大家可以自行去尝试了 + +## 总结 + +相信有以上四种调试技巧的加成,无论是工作debug 还是私下阅读源码,都可以轻松驾驭了。最后,来看看 IDEA 支持的各种断点调试类型,如果你只知道红色小圆点,那咱在留言区好好说说吧 + +![](http://cdn.tobebetterjavaer.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) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/ide/xechat.md b/docs/ide/xechat.md new file mode 100644 index 0000000000..60177c6e87 --- /dev/null +++ b/docs/ide/xechat.md @@ -0,0 +1,163 @@ +--- +title: 在 IDEA 里下个五子棋不过分吧? +shortTitle: 在IDEA里下五子棋 +category: + - Java企业级开发 +tag: + - Intellij IDEA + - IDEA + - Intellij IDEA 插件 + - IDEA 插件 +description: 在 IDEA 里下个五子棋不过分吧? +head: + - - meta + - name: keywords + content: Intellij IDEA,IDEA,Intellij IDEA 插件,IDEA 插件 + - name: description + content: 在 IDEA 里下个五子棋不过分吧? +--- + + +大家好,我是二哥呀! + +今天给大家分享一个基于Netty的IDEA即时聊天插件,可以实现即时聊天、游戏对战(下棋)。 + +>GitHub 地址:[https://github.com/anlingyi/xechat-idea](https://github.com/anlingyi/xechat-idea) + + +![](http://cdn.tobebetterjavaer.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) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-a6259f78-ded1-4aa9-aa35-3b7bc3ad823b.png) + +之后搜索关键字「xechat」安装插件。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-4169833e-5ed6-47f5-8e8c-03ea92400bc9.png) + +重启 Intellij IDEA 后在右下角找到 xechat 面板。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-a03023a2-0a7b-42d5-8fd6-67c494ef83b3.png) + +## 功能介绍 + +第一次打开后,会提示对应命令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-0de879be-2a64-4c85-b9ee-e92500a0a907.png) + +输入 `#login 沉默王二` 就可以登录了。 之后就可以把天聊起来了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-020bfafc-8874-4fda-a9a0-b9ac9d628234.png) + +使用复制粘贴还可以发送图片,虽然体验比较迟钝,延迟比较高,但真的是**又不是不能用**。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-5570fa5f-88d3-4f4f-882b-89a30bb9ef19.png) + +## 开始游戏 + +输入 `#showGame` 可以查看支持的游戏,目前支持五子棋、斗地主两种游戏。 + +输入 `#play 0` 开启五子棋启动面板。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-aff1ec60-b56e-4ab2-8e2f-237160eeb68c.png) + +卧槽,第一局竟然输了! + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-bca643f7-8615-4c12-ab05-f65600fbcfde.png) + +我太菜了,要怪只能怪作者设置的这个棋盘设置得太小了,竟然布局不能调整,哼。 + +呵呵呵,果不其然,放大以后再来一盘,稳稳赢了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-529c038d-d4a2-43fa-90f9-827648ebf6f7.png) + +嘿嘿,果然爽。 + +## 部署服务端 + +直接在 Intellij IDEA 中运行 xechat 插件的话,是共享的 xechat 的服务器,这不,竟然遇到了作者,竟然还是二哥的读者。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-45ff0d90-c777-47b9-8b18-b26c44e4c3f1.png) + +想要自己在本地把服务跑起来也很简单,从 GitHub 仓库把源代码拉到本地。 + +先进入 xechat-commons 包执行 `mvn install`,公共模块需优先打包。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-490a3ed3-628a-47a9-b262-c0bff8259f89.png) + +再进入 xechat-server 包执行 `mvn package` 打包。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-f6611304-1293-4ad2-97a5-9a685fc64575.png) + +之后执行 `java -jar target/xechat-server-xxx.jar -p 1024` 运行服务端。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-8f2524b0-dfaf-43ff-be3b-b71763ffcdcf.png) + +再次进入 Intellij IDEA 的 xechat 面板,输入 `#login -h 127.0.0.1 -p 1024` 就可以连上本地服务了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-0df4b0c3-dfae-4b15-9f0b-ff0b9326e0bc.png) + +OK,搞定。 + +## 学习源码 + +之前有小伙伴问我 JavaSE 部分的源码有没有推荐的,那这个 xechat 就是非常不错的选择。 + +我 down 到本地看了一下,代码整体来说还是非常优秀的,尤其是 Netty 部分,是非常值得参考和借鉴的。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-1d8e2c9f-14d9-486d-939f-75643d896a59.png) + +可以直接从 main 方法开始,一路 debug 下去看一看,我觉得是一个挺不错的选择。 + + +## ending + +一个人可以走得很快,但一群人才能走得更远。欢迎加入[二哥的编程星球](https://mp.weixin.qq.com/s/3RVsFZ17F0JzoHCLKbQgGw),里面的每个球友都非常的友善,除了鼓励你,还会给你提出合理的建议。 + + + +![基本上所有问题球友都能给出答案](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-a97b7195-7644-436b-872c-8855932aa0e6.png) + + + +星球提供的三份专属专栏《Java 面试指南》、《编程喵🐱(Spring Boot+Vue 前后端分离)实战项目笔记》、《Java 版 LeetCode 刷题笔记》,干货满满,价值连城。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-1b66f9ac-b530-4f66-8edf-39359ef02340.png) + + +已经有 **420 多名** 小伙伴加入[二哥的编程星球](https://mp.weixin.qq.com/s/3RVsFZ17F0JzoHCLKbQgGw)了,如果你也需要一个良好的学习氛围,[戳链接](https://mp.weixin.qq.com/s/3RVsFZ17F0JzoHCLKbQgGw)加入我们的大家庭吧!这是一个 Java 学习指南 + 编程实战 + LeetCode 刷题的私密圈子,你可以向二哥提问、帮你制定学习计划、跟着二哥一起做实战项目,冲冲冲。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/xechat-aebece5e-86a1-4950-a2b4-bd6c20dbc030.png) + + + +--- + +*没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟*。 + + +**本文已收录到 GitHub 上星标 2.9k+ 的开源专栏《Java 程序员进阶之路》,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、Nginx、Maven、Intellij IDEA、Spring、Spring Boot、Redis、MySql 等等)、Java 面试等核心知识点。学 Java,就认准 Java 程序员进阶之路**😄。 + +[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/interview/java-34.md b/docs/interview/java-34.md similarity index 92% rename from docs/src/interview/java-34.md rename to docs/interview/java-34.md index 33be9a6679..4a746cfb0f 100644 --- a/docs/src/interview/java-34.md +++ b/docs/interview/java-34.md @@ -5,24 +5,26 @@ category: - 求职面试 tag: - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,34 道 Java 精选面试题👍 +description: Java程序员进阶之路,小白的零基础Java教程,34 道 Java 精选面试题👍 head: - - meta - name: keywords content: Java,java,面试题,八股文 --- +# Java:34道精选高频面试题必看:+1: + ## 1.介绍一下 java 吧 java 是一门**开源的跨平台的面向对象的**计算机语言. -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-01.png) 跨平台是因为 java 的 class 文件是运行在虚拟机上的,其实跨平台的,而**虚拟机是不同平台有不同版本**,所以说 java 是跨平台的. 面向对象有几个特点: -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-02.png) - 1.**封装** - 两层含义:一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者'封装'在一个不可分割的**独立单元**(即对象)中 @@ -60,7 +62,7 @@ java 是一门**开源的跨平台的面向对象的**计算机语言. ## 2.java 有哪些数据类型? -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-03.png) java 主要有两种数据类型 @@ -136,7 +138,7 @@ try{ ## 7.arrayList 和 linkedList 的区别? -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-04.png) - 1.ArrayList 是实现了基于**数组**的,存储空间是连续的。LinkedList 基于**链表**的,存储空间是不连续的。(LinkedList 是双向链表) @@ -165,7 +167,7 @@ try{ ## 10.那么 hashMap 线程不安全怎么解决? -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-05.png) - 一.给 hashMap **直接加锁**,来保证线程安全 - 二.使用 **hashTable**,比方法一效率高,其实就是在其方法上加了 synchronized 锁 @@ -183,7 +185,7 @@ try{ ## 12.介绍一下 hashset 吧 -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-06.png) 上图是 set 家族整体的结构, @@ -209,7 +211,7 @@ HashSet 是**基于 HashMap 实现**的,底层**采用 HashMap 来保存元素 ## 16.volatile 有什么作用? -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-07.png) - **1.保证内存可见性** - 可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。 @@ -245,7 +247,7 @@ Integer a = 1,Integer b = 1,a==b 结果为**true** ## 19.JMM 是什么? -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-08.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-08.png) JMM 就是 **Java内存模型**(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)**屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果**。 @@ -324,7 +326,7 @@ public class Singleton { ## 22.volatile 有什么作用 -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-09.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-09.png) - 1.**保证内存可见性** - 当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量**写入数据**的时候,虚拟机会**强制它被值刷新到主内存中**。当一个线程**读取**被volatile关键字修饰的值的时候,虚拟机会**强制要求它从主内存中读取**。 @@ -355,7 +357,7 @@ try { 在 Java1.6 之前的版本中,synchronized 属于重量级锁,效率低下,**锁是** cpu 一个**总量级的资源**,每次获取锁都要和 cpu 申请,非常消耗性能。 在 **jdk1.6 之后** Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了,Jdk1.6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,**增加了锁升级的过程**,由无锁->偏向锁->自旋锁->重量级锁 -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-10.png) 增加锁升级的过程主要是**减少用户态到核心态的切换,提高锁的效率,从 jvm 层面优化锁** @@ -369,7 +371,7 @@ cas 叫做 CompareAndSwap,**比较并交换**,很多地方使用到了它, 当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,**如果相同就认为其他线程没有进行修改**,而将新值写入内存 -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-11.png) **CAS的缺点** @@ -390,7 +392,7 @@ ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。 **公平锁** -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-12.png) - 第一步:**获取状态的 state 的值** - 如果 state=0 即代表锁没有被其它线程占用,执行第二步。 @@ -404,7 +406,7 @@ ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。 **非公平锁** -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-13.png) - 获取状态的 state 的值 - 如果 state=0 即代表锁没有被其它线程占用,则设置当前锁的持有者为当前线程,该操作用 CAS 完成。 @@ -507,7 +509,7 @@ public class Demo { ## 29.线程池的执行流程? -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-14.png) - 判断线程池中的线程数**是否大于设置的核心线程数** - 如果**小于**,就**创建**一个核心线程来执行任务 @@ -555,13 +557,13 @@ Object obj = new Object(); ## 33.聊聊 ThreadLocal 吧 - ThreadLocal其实就是**线程本地变量**,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-15.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-16.png) - **1.对象头**: 对象头又分为 **MarkWord** 和 **Class Pointer** 两部分。 @@ -578,10 +580,9 @@ Object obj = new Object(); --------- -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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/interview/java-basic-baguwen.md b/docs/interview/java-basic-baguwen.md similarity index 96% rename from docs/src/interview/java-basic-baguwen.md rename to docs/interview/java-basic-baguwen.md index c44c4f0736..841848c3c7 100644 --- a/docs/src/interview/java-basic-baguwen.md +++ b/docs/interview/java-basic-baguwen.md @@ -1,17 +1,19 @@ --- title: Java 基础背诵版八股文必看🍉 -shortTitle: Java基础背诵版八股文🍉 +shortTitle: Java 基础背诵版八股文必看🍉 category: - 求职面试 tag: - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,Java 基础背诵版八股文必看🍉 +description: Java程序员进阶之路,小白的零基础Java教程,Java 基础背诵版八股文必看🍉 head: - - meta - name: keywords content: Java,java,面试题,八股文 --- +# Java 基础八股文(背诵版)必看🍉 + ### Java 语言具有哪些特点? - Java 为纯面向对象的语言。它能够直接反应现实生活中的对象。 @@ -407,10 +409,9 @@ equals 和 hashCode 这两个方法都是从 object 类中继承过来的,equa --------- -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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/interview/java-hashmap-13.md b/docs/interview/java-hashmap-13.md similarity index 90% rename from docs/src/interview/java-hashmap-13.md rename to docs/interview/java-hashmap-13.md index 7b00f4851d..8a140bcb41 100644 --- a/docs/src/interview/java-hashmap-13.md +++ b/docs/interview/java-hashmap-13.md @@ -1,17 +1,19 @@ --- title: 13 道 Java HashMap 精选面试题👍 -shortTitle: 13道HashMap精选面试题👍 +shortTitle: 13 道 Java HashMap 精选面试题👍 category: - 求职面试 tag: - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,13 道 Java HashMap 精选面试题👍 +description: Java程序员进阶之路,小白的零基础Java教程,13 道 Java HashMap 精选面试题👍 head: - - meta - name: keywords content: Java,java,hashmap,面试题,八股文 --- +# Java HashMap精选面试题 + 对于 Java 求职者来说,HashMap 可谓是重中之重,是面试的必考点。然而 HashMap 的知识点非常多,复习起来花费精力很大。 @@ -53,7 +55,7 @@ static final int MIN_TREEIFY_CAPACITY = 64; JDK 8 中 HashMap 的结构示意图: -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-interview-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-interview-01.png) ### 02、为什么链表改为红黑树的阈值是 8? @@ -135,7 +137,7 @@ HashMap中采用的是链地址法 。 2 的 N 次幂有助于减少碰撞的几率。如果 length 为2的幂次方,则 length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费。我们来举个例子,看下图: -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-interview-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-interview-02.png) 当 length =15时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,4和5的结果也是一样,这样就会导致查询速度降低。 @@ -175,7 +177,7 @@ static final int tableSizeFor(int cap) { 6、如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-interview-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-interview-03.png) ### 11、HashMap 的扩容方式? @@ -204,11 +206,10 @@ 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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/interview/java-jvm-baguwen.md b/docs/interview/java-jvm-baguwen.md similarity index 94% rename from docs/src/interview/java-jvm-baguwen.md rename to docs/interview/java-jvm-baguwen.md index 25f288ceea..fc936087a8 100644 --- a/docs/src/interview/java-jvm-baguwen.md +++ b/docs/interview/java-jvm-baguwen.md @@ -1,17 +1,19 @@ --- title: Java 虚拟机背诵版八股文必看🍉 -shortTitle: JVM背诵版八股文🍉 +shortTitle: Java 虚拟机背诵版八股文必看🍉 category: - 求职面试 tag: - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,Java 虚拟机背诵版八股文必看🍉 +description: Java程序员进阶之路,小白的零基础Java教程,Java 虚拟机背诵版八股文必看🍉 head: - - meta - name: keywords content: Java,java,jvm,面试题,八股文 --- +# Java 虚拟机八股文(背诵版)必看:+1: + ### 简述JVM内存模型 线程私有的运行时数据区: 程序计数器、Java 虚拟机栈、本地方法栈。 @@ -211,10 +213,9 @@ MinorGC 前,虚拟机必须检查老年代最大可用连续空间是否大于 --------- -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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/interview/java-thread-baguwen.md b/docs/interview/java-thread-baguwen.md similarity index 95% rename from docs/src/interview/java-thread-baguwen.md rename to docs/interview/java-thread-baguwen.md index 14e9e8ef3c..3a416d428e 100644 --- a/docs/src/interview/java-thread-baguwen.md +++ b/docs/interview/java-thread-baguwen.md @@ -1,20 +1,20 @@ --- title: Java 并发编程背诵版八股文必看🍉 -shortTitle: 并发编程背诵版八股文🍉 +shortTitle: Java 并发编程背诵版八股文必看🍉 category: - 求职面试 tag: - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,Java 并发编程背诵版八股文必看🍉 +description: Java程序员进阶之路,小白的零基础Java教程,Java 并发编程背诵版八股文必看🍉 head: - - meta - name: keywords content: Java,java,thread,面试题,八股文 --- +# Java 并发编程八股文(背诵版)必看:+1: ### 简述Java内存模型(JMM) - Java内存模型定义了程序中各种变量的访问规则: - 所有变量都存储在主存,每个线程都有自己的工作内存。 @@ -22,11 +22,9 @@ Java内存模型定义了程序中各种变量的访问规则: - 操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。 ### 简述as-if-serial - 编译器会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果都必须和用户原始程序输出的预定结果保持一致。 ### 简述happens-before八大规则 - - 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作; - 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作; - volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作; @@ -37,34 +35,26 @@ Java内存模型定义了程序中各种变量的访问规则: - 对象终结规则:一个对象的初始化完成先行发生于他的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 方法。 @@ -75,14 +65,11 @@ as-if-serial 保证单线程程序的执行结果不变,happens-before 保证 - TERMINATED:结束状态。线程调用完run方法进入该状态。 ### 简述线程通信的方式 - - volatile 关键词修饰变量,保证所有线程对变量访问的可见性。 - synchronized关键词。确保多个线程在同一时刻只能有一个处于方法或同步块中。 - wait/notify方法 - IO通信 - ### 简述线程池 - 没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后复用已创建的线程,可以降低开销、控制最大并发数。 线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。 @@ -112,11 +99,9 @@ as-if-serial 保证单线程程序的执行结果不变,happens-before 保证 ### 简述Executor框架 - Executor框架目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。 ### 简述Executor的继承关系 - - Executor:一个接口,其定义了一个接收Runnable对象的方法executor,该方法接收一个Runable实例执行这个任务。 - ExecutorService:Executor的子类接口,其定义了一个接收Callable对象的方法,返回 Future 对象,同时提供execute方法。 - ScheduledExecutorService:ExecutorService的子类接口,支持定期执行任务。 @@ -124,8 +109,7 @@ Executor框架目的是将任务提交和任务如何运行分离开来的机制 - Executors:实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。 - ThreadPoolExecutor:继承AbstractExecutorService,用于创建线程池。 - ForkJoinPool: 继承AbstractExecutorService,Fork 将大任务分叉为多个小任务,然后让小任务执行,Join 是获得小任务的结果,类似于map reduce。 -- ScheduledThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建带定时任务的线程池。 - +- ThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建带定时任务的线程池。 ### 简述线程池的状态 - Running:能接受新提交的任务,也可以处理阻塞队列的任务。 - Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方法,会进入该状态。 @@ -327,10 +311,9 @@ AQS获取共享锁/释放共享锁原理 --------- -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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/interview/mianshiguan-bigfile-miaochuan.md b/docs/interview/mianshiguan-bigfile-miaochuan.md new file mode 100644 index 0000000000..487d85a389 --- /dev/null +++ b/docs/interview/mianshiguan-bigfile-miaochuan.md @@ -0,0 +1 @@ +# 大文件上传时如何做到秒传? 大家好,我是二哥呀~ 文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用这种方式进行上传,可不是一个好的办法,毕竟很少有用户能忍受,尤其是当文件上传到一半中断后,继续上传却只能重头开始上传,让用户的体验尤其不爽。 那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式。 ## 秒传 ### 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) ---- 最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) 关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 ![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/interview/mianshiguan-fenkufenbiao.md b/docs/interview/mianshiguan-fenkufenbiao.md similarity index 84% rename from docs/src/interview/mianshiguan-fenkufenbiao.md rename to docs/interview/mianshiguan-fenkufenbiao.md index 30083d771f..3efa045b29 100644 --- a/docs/src/interview/mianshiguan-fenkufenbiao.md +++ b/docs/interview/mianshiguan-fenkufenbiao.md @@ -1,5 +1,5 @@ --- -title: 阿里面试官:为什么要分库分表? +title: 阿里面试官👤:为什么要分库分表? shortTitle: 为什么要分库分表? author: 苏三呀 category: @@ -32,7 +32,7 @@ head: 系统初期的数据库架构如下: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-965dbc52-1fad-4cf7-a6fc-f04ff445101f.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-965dbc52-1fad-4cf7-a6fc-f04ff445101f.jpg) 此时,使用的数据库方案是:`一个数据库`包含`多张业务表`。用户读数据请求和写数据请求,都是操作的同一个数据库。 @@ -48,7 +48,7 @@ head: 将`用户表`拆分为:`用户基本信息表` 和 `用户扩展表`。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-32cbe5e1-b88b-4125-b990-8f901467fbb2.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-32cbe5e1-b88b-4125-b990-8f901467fbb2.jpg) 用户基本信息表中存的是用户最主要的信息,比如:用户名、密码、别名、手机号、邮箱、年龄、性别等核心数据。 @@ -76,7 +76,7 @@ head: 具体拆分过程如下: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-9cab165b-f2e0-4c80-9bd1-cc272a78429c.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-9cab165b-f2e0-4c80-9bd1-cc272a78429c.jpg) 将用户、产品、物流、订单相关的表,从原来一个数据库中,拆分成单独的用户库、产品库、物流库和订单库,一共四个数据库。 @@ -92,7 +92,7 @@ head: 每年都有个单独的数据库,每个数据库中,都有12张表,每张表存储一个月的用户资金数据。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-38434e50-9dce-467f-a036-e75038d9f3f6.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-38434e50-9dce-467f-a036-e75038d9f3f6.jpg) 这样分库分表之后,就能非常高效的查询出某个用户每个月,或者每年的资金了。 @@ -110,7 +110,7 @@ head: 这时的数据库架构如下: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-965dbc52-1fad-4cf7-a6fc-f04ff445101f.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-965dbc52-1fad-4cf7-a6fc-f04ff445101f.jpg) 此时,使用的数据库方案同样是:`一个master数据库`包含`多张业务表`。 @@ -132,7 +132,7 @@ head: 于是,就出现了主从读写分离架构: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-e56c2d22-9aa2-4502-8d2a-f4416ce20db9.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-e56c2d22-9aa2-4502-8d2a-f4416ce20db9.jpg) 考虑刚开始用户量还没那么大,选择的是`一主一从`的架构,也就是常说的一个master一个slave。 @@ -150,13 +150,13 @@ head: 这就需要`一主多从`的架构了: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-1fc49e99-76ac-4aea-8c3d-e438de2cdd1c.jpg) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-bbf8495d-2338-4bcf-b8e3-cebaea4fac29.jpg) 这样就能解决上面的问题了。 @@ -172,7 +172,7 @@ head: 用户库的拆分过程如下: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-f2354590-0161-45e7-afb7-4ad07a3763fb.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-f2354590-0161-45e7-afb7-4ad07a3763fb.jpg) 在这里我将用户库拆分成了三个库(真实场景不一定是这样的),每个库的表结构是一模一样的,只有存储的数据不一样。 @@ -192,7 +192,7 @@ head: 表拆分过程如下: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-8830dc2f-b088-4190-bc9a-a19bf55de99b.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-8830dc2f-b088-4190-bc9a-a19bf55de99b.jpg) 我在这里将用户库中的用户表,拆分成了四张表(真实场景不一定是这样的),每张表的表结构是一模一样的,只是存储的数据不一样。 @@ -206,7 +206,7 @@ head: 如下图所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-1cfacfbf-283a-4415-91da-c0c2616c2f0a.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-1cfacfbf-283a-4415-91da-c0c2616c2f0a.jpg) 图中将用户库拆分成了三个库,每个库都包含了四张用户表。 @@ -239,7 +239,7 @@ head: 为了解决这两个问题,我们当时采用的方案是:`分库`。即针对每一个游戏都单独建一个数据库,数据库中的表结构允许存在差异。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-f9f55b53-0465-48d1-9715-241129fe7833.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-f9f55b53-0465-48d1-9715-241129fe7833.jpg) 我们当时没有进一步分表,是因为当时考虑每种游戏的用户量,还没到大到离谱的地步。不像王者荣耀这种现象级的游戏,有上亿的玩家。 @@ -271,7 +271,7 @@ head: 当时使用一个积分数据库就够了,但是分了128张表。然后根据用户id,进行hash除以128取模。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-884b58a4-3009-4056-904d-cd235db41aa6.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-884b58a4-3009-4056-904d-cd235db41aa6.jpg) > 需要特别注意的是,分表的数量最好是2的幂次方,方便以后扩容。 @@ -289,7 +289,7 @@ head: 当时分了4个库,每个库有32张表。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-ed3d9006-7dd5-4f90-a566-8874020df4ea.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-ed3d9006-7dd5-4f90-a566-8874020df4ea.jpg) ## 4 总结 @@ -314,9 +314,8 @@ head: ---- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/interview/mianshiguan-youhuiquan.md b/docs/interview/mianshiguan-youhuiquan.md similarity index 78% rename from docs/src/interview/mianshiguan-youhuiquan.md rename to docs/interview/mianshiguan-youhuiquan.md index f6c189de64..890c7d8af7 100644 --- a/docs/src/interview/mianshiguan-youhuiquan.md +++ b/docs/interview/mianshiguan-youhuiquan.md @@ -1,6 +1,6 @@ --- -title: 淘宝面试官:优惠券系统该如何设计? -shortTitle: 如何设计优惠券系统? +title: 淘宝面试官👤:优惠券系统该如何设计? +shortTitle: 大厂的优惠券系统是如何设计的? description: 大厂的优惠券系统是如何设计的? category: - 求职面试 @@ -76,7 +76,7 @@ head: ### **2.1 服务结构设计** -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-cdf44a4a-f648-49f0-bebe-686c4a66d2fd.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-cdf44a4a-f648-49f0-bebe-686c4a66d2fd.jpg) ### **2.2 优惠券系统设计技术难点** @@ -107,15 +107,15 @@ head: 优惠券的使用有规则和条件限制,比如满100减50券,需要达到门槛金额100元才能使用。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-638df6ed-12f8-4a20-8c58-89051bc9376a.jpg) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-b2383938-ffbc-4686-808c-908ab11e4b2b.jpg) **规则内容:** @@ -178,9 +178,9 @@ VALUES(“劳斯莱斯5元代金券”, 1010, 10000); ### **3.3 发券** -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-066459b1-1271-468e-9e45-c409b6298144.jpg) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-a448c453-8b50-4af8-a913-bda03a6c6f97.jpg) ##### **如何给大量用户发券?** @@ -219,11 +219,11 @@ comment '信息表'; **信息表 message** -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-19c6ce5d-8e83-4552-902d-5d28cd17a7be.jpg) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-e04b414f-6b41-4882-8324-208715b52284.jpg) #### **发一封站内信的步骤** @@ -271,7 +271,7 @@ create table t_message_content #### **给 10W 用户发券** -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-e6134270-a52f-4579-a200-e0c0e4c13c82.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-e6134270-a52f-4579-a200-e0c0e4c13c82.jpg) 有什么问题?重复消费,导致超发! @@ -318,7 +318,7 @@ UPDATE coupon_batch SET total_count = total_count - 1, 用户领券过程中,其实也会出现类似秒杀场景。秒杀场景下会有哪些问题,如何解决? -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-8367d61b-2a12-4225-9fc2-586a1baac77d.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-8367d61b-2a12-4225-9fc2-586a1baac77d.jpg) #### **用户重复领取或多领** @@ -361,7 +361,7 @@ SADD batch_id:1111:user_id 1001 ### **返回可用券** -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-182ea6b8-4702-4f01-b3dd-f9636815863f.jpg) +![](http://cdn.tobebetterjavaer.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; @@ -371,11 +371,11 @@ 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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-3b30f2df-fd77-4265-b655-31a82028505e.jpg) ### **表设计** @@ -411,7 +411,7 @@ TCC,Try-Confirm-Cancel,目前分布式事务主流解决方案。 支付失败/超时或订单关闭情况,将优惠券状态改为 “未使用” -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-c6a77fdf-4f4d-4082-92a7-c2cd11aba686.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-c6a77fdf-4f4d-4082-92a7-c2cd11aba686.jpg) ## 4 Scale 扩展 @@ -459,9 +459,9 @@ alter table t_notify_msg ### **4.2 数据库层面优化 - 索引** -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-9e10c3cb-d0af-4b0e-a2f9-3da2fe7e72f1.jpg) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-8df5b954-9e5a-445d-882f-f91616fee662.jpg) ### **4.3 发券接口,限流保护** @@ -469,7 +469,7 @@ alter table t_notify_msg 点击一次后,按钮短时间内置灰 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-d2d8684d-9488-4543-9b2b-8e7559c8e625.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-d2d8684d-9488-4543-9b2b-8e7559c8e625.jpg) #### **后端限流** @@ -480,9 +480,8 @@ alter table t_notify_msg ---- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/interview/mysql-60.md b/docs/interview/mysql-60.md similarity index 96% rename from docs/src/interview/mysql-60.md rename to docs/interview/mysql-60.md index 28ac739a0a..ea611c5400 100644 --- a/docs/src/interview/mysql-60.md +++ b/docs/interview/mysql-60.md @@ -5,7 +5,7 @@ category: - 求职面试 tag: - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,60 道 MySQL 精选面试题👍 +description: Java程序员进阶之路,小白的零基础Java教程,60 道 MySQL 精选面试题👍 head: - - meta - name: keywords @@ -13,6 +13,8 @@ head: --- +# 精选数据库60道面试题必看:+1: + > 图文详解 60 道 MySQL 面试高频题,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/c-sy7tM0BmrqMUQFW7C65g),里面有局详细的思维导图;作者:herongwei,戳[原文链接](https://mp.weixin.qq.com/s/-SqqKmhZcOlQxM-rHiIpKg)。 @@ -143,7 +145,7 @@ Server 层按顺序执行 SQL 的步骤为: ### 13、MySQL 的 redo log 和 binlog 区别? -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-a2b8e123-41cb-4717-9225-3a8b49197004.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/herongwei/mysql-a2b8e123-41cb-4717-9225-3a8b49197004.png) ### 14、为什么需要 redo log? @@ -181,7 +183,7 @@ redo log包括两部分内容,分别是内存中的**日志缓冲**(redo log b 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) +![](http://cdn.tobebetterjavaer.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进行配置,参数值含义如下: @@ -198,7 +200,7 @@ MySQL 每执行一条 DML 语句,会先把记录写入 **redo log buffer(用 update T set a =1 where id =666 ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-43fe6587-0cb8-49aa-bd93-0119e46430d7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/herongwei/mysql-43fe6587-0cb8-49aa-bd93-0119e46430d7.png) 1. MySQL 客户端将请求语句 update T set a =1 where id =666,发往 MySQL Server 层。 @@ -220,7 +222,7 @@ update T set a =1 where id =666 MySQL 将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,这就是"两阶段提交"。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-11420486-f9d0-483a-ba2e-a742ec4c518d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/herongwei/mysql-11420486-f9d0-483a-ba2e-a742ec4c518d.png) 而两阶段提交就是让这两个状态保持逻辑上的一致。redolog 用于恢复主机故障时的未更新的物理数据,binlog 用于备份操作。两者本身就是两个独立的个体,要想保持一致,就必须使用分布式事务的解决方案来处理。 @@ -275,7 +277,7 @@ WAL,中文全称是 Write-Ahead Logging,它的关键点就是日志先写内 ### 24、redo log日志格式 -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-ee8a859f-d1e8-4ab6-94d1-9733373be825.png) +![](http://cdn.tobebetterjavaer.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。 @@ -626,11 +628,10 @@ MySQL 主备切换流程: --------- -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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/interview/nginx-40.md b/docs/interview/nginx-40.md similarity index 95% rename from docs/src/interview/nginx-40.md rename to docs/interview/nginx-40.md index f901061d95..aa8b59537f 100644 --- a/docs/src/interview/nginx-40.md +++ b/docs/interview/nginx-40.md @@ -5,13 +5,14 @@ category: - 求职面试 tag: - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,40 道 Nginx 精选面试题👍 +description: Java程序员进阶之路,小白的零基础Java教程,40 道 Nginx 精选面试题👍 head: - - meta - name: keywords content: Nginx,nginx,面试题,八股文 --- +# Nginx:40道精选面试题必看:+1: 大家好,我是二哥呀!那天,我徒弟小二偷偷摸摸跑去了一家公司面试,结果回来给我说在 Nginx 上跪了,问我该怎么办? @@ -56,7 +57,7 @@ head: * 生产中如何设置worker进程的数量呢? * nginx状态码 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-63553177-7359-4f68-8673-5b44285cb701.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nginx/40-interview-63553177-7359-4f68-8673-5b44285cb701.png) @@ -239,7 +240,7 @@ session: 最核心的区别在于apache是同步多进程模型,一个连接对应一个进程,nginx是异步的,多个连接可以对应一个进程。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-8cd652f4-5a93-4e12-a020-0d90b9379bf2.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nginx/40-interview-8cd652f4-5a93-4e12-a020-0d90b9379bf2.png) ## 什么是动态资源、静态资源分离? @@ -440,7 +441,7 @@ location的语法能说出来吗? > 注意:~ 代表自己输入的英文字母 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-8b8c7eeb-5542-43ab-9acc-3c7e51b5a4ac.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nginx/40-interview-8b8c7eeb-5542-43ab-9acc-3c7e51b5a4ac.png) #### Location正则案例 @@ -561,7 +562,7 @@ server { 漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-57f54ac6-82fe-403c-af37-728d707d5eca.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nginx/40-interview-57f54ac6-82fe-403c-af37-728d707d5eca.png) #### 令牌桶算法 @@ -570,7 +571,7 @@ server { 系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-4bebeaf2-3853-4bb3-a34d-10316a230854.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nginx/40-interview-4bebeaf2-3853-4bb3-a34d-10316a230854.png) ## Nginx配置高可用性怎么配置? @@ -697,17 +698,17 @@ http { 我们可以先来对比下,如果我们没有开启zip压缩之前,我们的对应的文件大小,如下所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-fb46fd48-e596-48ac-8759-0663d29593af.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nginx/40-interview-c3c95717-45ee-4f56-b606-d56d2fc3cc57.png) gzip压缩前后效果对比:jquery原大小90kb,压缩后只有30kb。 @@ -795,10 +796,9 @@ Proxy_set_header THE-TIME $date_gmt; --------- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/interview/redis-12.md b/docs/interview/redis-12.md similarity index 86% rename from docs/src/interview/redis-12.md rename to docs/interview/redis-12.md index 89e582ee69..314ce3c1b9 100644 --- a/docs/src/interview/redis-12.md +++ b/docs/interview/redis-12.md @@ -5,13 +5,14 @@ category: - 求职面试 tag: - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,12 道 Redis 精选面试题👍 +description: Java程序员进阶之路,小白的零基础Java教程,12 道 Redis 精选面试题👍 head: - - meta - name: keywords content: Redis,redis,面试题,八股文 --- +# Redis:12道精选高频面试题 大家好,我是二哥呀。 @@ -62,7 +63,7 @@ AOF 采用的是写后日志的方式,Redis 先执行命令把数据写入内 -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mianjing/redis12question-1.png) RDB 采用的是内存快照的方式,它记录的是某一时刻的数据,而不是操作,所以采用 RDB 方法做故障恢复时只需要直接把 RDB 文件读入内存即可,实现快速恢复。 @@ -95,7 +96,7 @@ RDB 采用的是内存快照的方式,它记录的是某一时刻的数据, 小二:额,这个我不太清楚... -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-2.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mianjing/redis12question-2.png) 面试官: @@ -106,7 +107,7 @@ RDB 采用的是内存快照的方式,它记录的是某一时刻的数据, - 如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mianjing/redis12question-3.png) 要注意,Redis 对 RDB 的执行频率非常重要,因为这会影响快照数据的完整性以及 Redis 的稳定性,所以在 Redis 4.0 后,增加了 AOF 和 RDB 混合的数据持久化机制: 把数据以 RDB 的方式写入文件,再将后续的操作命令以 AOF 的格式存入文件,既保证了 Redis 重启速度,又降低数据丢失风险。 @@ -121,21 +122,21 @@ RDB 采用的是内存快照的方式,它记录的是某一时刻的数据, 将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,这个跟 MySQL 主从复制的原理一样。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mianjing/redis12question-4.png) **2)哨兵模式** 使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis 增加了哨兵模式(因为哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-5.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mianjing/redis12question-6.png) 面试官:使用哨兵模式在数据上有副本数据做保证,在可用性上又有哨兵监控,一旦 master 宕机会选举 salve 节点为 master 节点,这种已经满足了我们的生产环境需要,**那为什么还需要使用集群模式呢**? @@ -146,7 +147,7 @@ Redis Cluster 是一种分布式去中心化的运行模式,是在 Redis 3.0 小二:这应该是使用了某种 hash 算法,但是我不太清楚。。。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mianjing/redis12question-7.png) 面试官:那好,今天的面试就到这里吧,你先回去等我们的面试通知。 @@ -179,10 +180,9 @@ C |10001 - 16383 --------- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/io/Serializbale.md b/docs/io/Serializbale.md new file mode 100644 index 0000000000..4be26101e0 --- /dev/null +++ b/docs/io/Serializbale.md @@ -0,0 +1,536 @@ +--- +title: Java Serializable:明明就一个空的接口嘛 +shortTitle: Serializable 接口 +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,Java序列化流,字节和对象之间的序列化和反序列化 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Serializable +--- + + +对于 Java 的序列化,我一直停留在最浅显的认知上——把那个要序列化的类实现 `Serializbale` 接口就可以了。我不愿意做更深入的研究,因为会用就行了嘛。 + + +但随着时间的推移,见到 `Serializbale` 的次数越来越多,我便对它产生了浓厚的兴趣。是时候花点时间研究研究了。 + +## 01、先来点理论 + + + +Java 序列化是 JDK 1.1 时引入的一组开创性的特性,用于将 Java 对象转换为字节数组,便于存储或传输。此后,仍然可以将字节数组转换回 Java 对象原有的状态。 + +序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。 + +再来看看序列化 `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 岁的王二”从文件中读出来,实际上就是一种反序列化的过程。 + +```java +public class Test { + + public static void main(String[] args) { + // 初始化 + 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()` 方法。其部分源码如下: + +``` +if (obj instanceof String) { + writeString((String) obj, unshared); +} else if (cl.isArray()) { + writeArray(obj, desc, unshared); +} else if (obj instanceof Enum) { + writeEnum((Enum) obj, desc, unshared); +} else if (obj instanceof Serializable) { + writeOrdinaryObject(obj, desc, unshared); +} 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++) { + + try { + writeObject0(objVals[i], + fields[numPrimFields + i].isUnshared()); + } + } + } +``` + +那怎么反序列化呢? + +以 `ObjectInputStream` 为例,它在反序列化的时候会依次调用 `readObject()`→`readObject0()`→`readOrdinaryObject()`→`readSerialData()`→`defaultReadFields()`。 + +```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++) { + + try { + writeObject0(objVals[i], + fields[numPrimFields + i].isUnshared()); + } + } + } +``` + +我想看到这,你应该会恍然大悟的“哦”一声了。`Serializable` 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成。 + +## 03、再来点注意事项 + +开门见山的说吧,`static` 和 `transient` 修饰的字段是不会被序列化的。 + +为什么呢?我们先来证明,再来解释原因。 + +首先,在 `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; + + int size = list.size(); + 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} + +## 05、再来点甜点 + +让我先问问你吧,你知道 ` private static final long serialVersionUID = -2095916884810199532L;` 这段代码的作用吗? + +嗯...... + +`serialVersionUID` 被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 `serialVersionUID` 与被序列化类中的 `serialVersionUID` 进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。 + +当一个类实现了 `Serializable` 接口后,IDE 就会提醒该类最好产生一个序列化 ID,就像下面这样: + +![](http://cdn.tobebetterjavaer.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` 竟然有这么多可以研究的内容! + +写完这篇文章之后,我不由得想起理科状元曹林菁说说过的一句话:“在学习中再小的问题也不放过,每个知识点都要总结”——说得真真真真的对啊! + + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/io/buffer.md b/docs/io/buffer.md new file mode 100644 index 0000000000..4ac342572e --- /dev/null +++ b/docs/io/buffer.md @@ -0,0 +1,411 @@ +--- +title: Java缓冲流(Buffered):读写速度有了质的飞升 +shortTitle: Java缓冲流Buffered +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,Java缓冲流(Buffered):读写速度有了质的飞升 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,IO,缓冲流,Buffered,BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter +--- + +## 1.1 简要概述 + +首先我们来认识认识缓冲流,也叫高效流,是对4个`FileXxx` 流的“增强流”。 + +**缓冲流的基本原理**: + +> 1、使用底层流对象从具体设备上获取数据,并将数据存储到缓冲区的数组内。 +> +> 2、通过缓冲区的read()方法从缓冲区获取具体的字符数据,这样就提高了效率。 +> +> 3、用read方法读取字符数据,并存储到另外一个容器中,直到读取到了换行符时,再将另一个容器临时存储的数据转成字符串返回,也就是readLine()的功能。 + +在创建缓冲流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写效率。 + +缓冲书写格式为`BufferedXxx`,按照数据类型分为 2类: + +* **字节缓冲流**:`BufferedInputStream`,`BufferedOutputStream` +* **字符缓冲流**:`BufferedReader`,`BufferedWriter` + +## 1.2 字节缓冲流 + +### 构造方法 + +* `public BufferedInputStream(InputStream in)` :创建一个新的缓冲输入流,注意参数类型为**InputStream**。 +* `public 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")); +``` + + +### 感受缓冲流的高效 + +缓冲流读写方法与基本的流是一致的,我们通过复制370多MB的大文件,测试它的效率。 + +1. 基本流,代码如下: + +```java +public class BufferedDemo { + public static void main(String[] args) throws FileNotFoundException { + // 记录开始时间 + long start = System.currentTimeMillis(); + // 创建流对象 + try ( + FileInputStream fis = new FileInputStream("py.exe");//exe文件够大 + FileOutputStream fos = new FileOutputStream("copyPy.exe") + ){ + // 读写数据 + int b; + while ((b = fis.read()) != -1) { + fos.write(b); + } + } catch (IOException e) { + e.printStackTrace(); + } + // 记录结束时间 + long end = System.currentTimeMillis(); + System.out.println("普通流复制时间:"+(end - start)+" 毫秒"); + } +} +不好意思十分钟过去了还在玩命复制中... +``` + + +2. 缓冲流,代码如下: + +```java +public class BufferedDemo { + public static void main(String[] args) throws FileNotFoundException { + // 记录开始时间 + long start = System.currentTimeMillis(); + // 创建流对象 + try ( + BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.exe")); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.exe")); + ){ + // 读写数据 + int b; + while ((b = bis.read()) != -1) { + bos.write(b); + } + } catch (IOException e) { + e.printStackTrace(); + } + // 记录结束时间 + long end = System.currentTimeMillis(); + System.out.println("缓冲流复制时间:"+(end - start)+" 毫秒"); + } +} + +缓冲流复制时间:8016 毫秒 +``` + + +有的童鞋就要说了,我要更快的速度!最近看速度与激情7有点上头,能不能再快些?答案是当然可以。 + +想要更快,可以使用数组的方式,代码如下: + +```java +public class BufferedDemo { + public static void main(String[] args) throws FileNotFoundException { + // 记录开始时间 + long start = System.currentTimeMillis(); + // 创建流对象 + try ( + BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.exe")); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.exe")); + ){ + // 读写数据 + int len; + byte[] bytes = new byte[8*1024]; + while ((len = bis.read(bytes)) != -1) { + bos.write(bytes, 0 , len); + } + } catch (IOException e) { + e.printStackTrace(); + } + // 记录结束时间 + long end = System.currentTimeMillis(); + System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒"); + } +} +缓冲流使用数组复制时间:521 毫秒 +``` + +### 为什么字节缓冲流会这么快呢? + +这恐怕也是大家看到这所想到的问题:“为什么字节缓冲流会这么快呢?” + +这就必须得说道说道了。 + +传统的 Java IO 是阻塞模式的,它的工作状态就是“读/写,等待,读/写,等待。。。。。。” + +字节缓冲流解决的就是这个问题:一次多读点多写点,减少读写的频率,用空间换时间。 + +我们来看 BufferedInputStream 的 read 方法: + +```java +public synchronized int read() throws IOException { + if (pos >= count) { + fill(); + if (pos >= count) + return -1; + } + return getBufIfOpen()[pos++] & 0xff; +} +``` + +这段代码主要有两部分: + +- fill():该方法会将缓冲 buf 填满 +- `getBufIfOpen()[pos++] & 0xff`:如果 buf 还没有读完,就从 buf 中获取一个字节,否则再次填充 buf + +>0xff 是十六进制,相当于二进制的 11111111,&运算符的意思是:如果两个操作数的对应位为 1,则输出 1,否则为 0;由于 0xff 有 8 个 1,单个 byte 转成 int 其实就是将 byte 和 int 类型的 255 进行(&)与运算即可,也就是 + +```java +byte b = 25; +int a = b & 0XFF; // 或者 b & 255; +``` + +[对 byte & 0xFF 的理解](http://ckjava.com/2018/05/03/java-byte-0XFF/) + +再来看 FileInputStream 的 read 方法: + +```java +public int read() throws IOException { + return read0(); +} + +private native int read0() throws IOException; +``` + +一个 native 方法(该方法的实现由非Java语言实现,比如C语言),它本质上是阻塞的,读取一个字节,然后阻塞,等待下一次调用。 + +画幅图比较一下。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/buffer-fcae80c2-04a5-4a1b-ab49-89a5ddabd38e.png) + +再来看 BufferedOutputStream 的 read 方法: + +```java +public synchronized void write(byte b[], int off, int len) throws IOException { + if (len >= buf.length) { + /* If the request length exceeds the size of the output buffer, + flush the output buffer and then write the data directly. + In this way buffered streams will cascade harmlessly. */ + flushBuffer(); + out.write(b, off, len); + return; + } + if (len > buf.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buf, count, len); + count += len; +} +``` + +只有当 buf 写满了,才会 flush,将数据刷到磁盘,默认一次刷 8192 个字节。 + +```java +public BufferedOutputStream(OutputStream out) { + this(out, 8192); +} +``` + +如果 buf 没有写满,会继续写 buf。 + +FileOutputStream 的 write 方法也是一个 native 方法,同样会阻塞。 + +```java +private native void write(int b, boolean append) throws IOException; + +public void write(int b) throws IOException { + write(b, append); +} +``` + +当 BufferedOutputStream 和 BufferedInputStream 配合起来后,就减少了相当大量的读写次数,尤其是 `byte[] bytes = new byte[8*1024]`,就相当于缓冲区的空间有 8 个 1024 字节了,那读写效率就会大大提高。 + +## 1.3 字符缓冲流 + +### 构造方法 + +相同的,来看看其构造,其格式以及原理和字节缓冲流是一样一样的! + +* `public BufferedReader(Reader in)` :创建一个新的缓冲输入流,注意参数类型为**Reader**。 +* `public BufferedWriter(Writer out)`: 创建一个新的缓冲输出流,注意参数类型为**Writer**。 + +构造举例,代码如下: + +```java +// 创建字符缓冲输入流 +BufferedReader br = new BufferedReader(new FileReader("b.txt")); +// 创建字符缓冲输出流 +BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt")); +``` + + +### 字符缓冲流特有方法 + +字符缓冲流的基本方法与普通字符流调用方式一致,这里不再阐述,我们来看字符缓冲流具备的**特有**方法。 + +* BufferedReader:`public String readLine()`: **读一行数据**。 读取到最后返回null +* BufferedWriter:`public void newLine()`: **换行**,由系统属性定义符号。 + +`readLine`方法演示代码如下: + +```java +public class BufferedReaderDemo { + public static void main(String[] args) throws IOException { + // 创建流对象 + 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 +public class BufferedWriterDemo throws IOException { + public static void main(String[] args) throws IOException { + // 创建流对象 + BufferedWriter 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(); + } +} +输出效果: +哥 +敢 +摸屎 +你敢吗? +``` + + +## 1.4 字符缓冲流练习 + +字符缓冲流练习啥捏?先放松一下吧各位,先欣赏欣赏我写的这篇诗 + +> 6.你说你的程序叫简单,我说我的代码叫诗篇 +> +> 1.一想到你我就哦豁豁豁豁豁豁豁豁豁豁....哦nima个头啊,完全不理人家受得了受不了 +> +> 8.Just 简单你和我 ,Just 简单程序员 +> +> 3.约了地点却忘了见面 ,懂得寂寞才明白浩瀚 +> +> 5.沉默是最大的发言权 +> +> 2.总是喜欢坐在电脑前, 总是喜欢工作到很晚 +> +> 7.向左走 又向右走,我们转了好多的弯 +> +> 4.你从来就不问我,你还是不是那个程序员 + +欣赏完了咩?没错,咱们就练习如何使用缓冲流的技术把上面的诗篇归顺序,都编过号了,就是前面的1到8的编号。 + +分析:首先用字符输入缓冲流创建个源,里面放没有排过序的文字,之后用字符输出缓冲流创建个目标接收,排序的过程就要自己写方法了哦,可以从每条诗词的共同点“.”符号下手! + +### 代码实现 + +```java +public class BufferedTest { + public static void main(String[] args) throws IOException { + // 创建map集合,保存文本数据,键为序号,值为文字 + HashMap lineMap = new HashMap<>(); + + // 创建流对象 源 + BufferedReader br = new BufferedReader(new FileReader("a.txt")); + //目标 + BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt")); + + // 读取数据 + String line = null; + while ((line = br.readLine())!=null) { + // 解析文本 + String[] split = line.split("\\."); + // 保存到集合 + 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(); + } +} +``` + + +运行效果 + +``` +1.一想到你我就哦豁豁豁豁豁豁豁豁豁豁…哦nima个头啊,完全不理人家受得了受不了 +2.总是喜欢坐在电脑前, 总是喜欢工作到很晚 +3.约了地点却忘了见面 ,懂得寂寞才明白浩瀚 +4.你从来就不问我,你还是不是那个程序员 +5.沉默是最大的发言权 +6.你说你的程序叫简单,我说我的代码叫诗篇 +7.向左走 又向右走,我们转了好多的弯 +8.Just 简单你和我 ,Just 简单程序员 +``` + +不过,由于字符流本身就用到了缓冲区,所以字符缓冲流就不像字节缓冲流那样,性能会有质的提升。 + +>参考链接:[https://www.cnblogs.com/yichunguo/p/11775270.html](https://www.cnblogs.com/yichunguo/p/11775270.html),整理:沉默王二 + + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/io/char-byte.md b/docs/io/char-byte.md new file mode 100644 index 0000000000..f108ec641d --- /dev/null +++ b/docs/io/char-byte.md @@ -0,0 +1,213 @@ +--- +title: Java转换流,解决字符与字节之间编码、解码的乱码问题 +shortTitle: Java转换流(编码解码与乱码) +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,Java转换流,解决字符与字节之间编码、解码的乱码问题 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java IO,转换流,InputStreamReader,OutputStreamWriter,乱码,编码,解码 +--- + +何谓转换流?为何由来?让我们暂时带着这两个问题来了解了解字符编码和字符集! + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/char-byte-86699b2c-4f24-492a-ba68-62c3be0f86bc.png) + +## 字符编码与解码 + +众所周知,计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。 + +按照某种规则,将字符存储到计算机中,称为**编码** 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为**解码** 。比如说,按照`A`规则存储,同样按照`A`规则解析,那么就能显示正确的文本符号。反之,按照`A`规则存储,再按照`B`规则解析,就会导致乱码现象。 + +简单一点的说就是: + +> 编码:字符(能看懂的)--字节(看不懂的) +> +> 解码:字节(看不懂的)-->字符(能看懂的) + +代码解释则是 + +```java +String(byte[] bytes, String charsetName):通过指定的字符集解码字节数组 +byte[] getBytes(String charsetName):使用指定的字符集合把字符串编码为字节数组 + +编码:把看得懂的变成看不懂的 +String -- byte[] + +解码:把看不懂的变成看得懂的 +byte[] -- String +``` + + +* **字符编码** `Character Encoding`: 就是一套自然语言的字符与二进制数之间的对应规则。 + +而**编码表**则是生活中文字和计算机中二进制的对应规则 + +## 字符集 + +* **字符集** `Charset`:也叫**编码表**。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。 + +计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见的字符集有`ASCII`字符集、`GBK`字符集、`Unicode`字符集等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/char-byte-eafc0ac8-ce5b-4183-9a7e-9498e23b2d4e.png) + +可见,当指定了**编码**,它所对应的**字符集**自然就指定了,所以**编码**才是我们最终要关心的。 + +* **ASCII字符集** : +* ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。 +* 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。 +* **ISO-8859-1字符集**: +* 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。 +* ISO-8859-1使用单字节编码,兼容ASCII编码。 +* **GBxxx字符集**: +* GB就是国标的意思,是为了显示中文而设计的一套字符集。 +* **GB2312**:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符。 +* **GBK**:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。 +* **GB18030**:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。 +* **Unicode字符集** : +* Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。 +* 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。 +* UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则: +1. 128个US-ASCII字符,只需一个字节编码。 +2. 拉丁文等字符,需要二个字节编码。 +3. 大部分常用字(含中文),使用三个字节编码。 +4. 其他极少使用的Unicode辅助字符,使用四字节编码。 + +## 编码问题导致乱码 + +在java开发工具IDEA中,使用`FileReader` 读取项目中的文本文件。由于IDEA的设置,都是默认的`UTF-8`编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统默认的是GBK编码,就会出现乱码。 + +```java +public class ReaderDemo { + public static void main(String[] args) throws IOException { + FileReader fileReader = new FileReader("C:\\a.txt"); + int read; + while ((read = fileReader.read()) != -1) { + System.out.print((char)read); + } + fileReader.close(); + } +} +输出结果:��� +``` + + +那么如何读取GBK编码的文件呢? 这个时候就得讲讲转换流了! + +> 从另一角度来讲:**字符流=字节流+编码表** + +## InputStreamReader类--(字节流到字符流的桥梁) + +转换流`java.io.InputStreamReader`,是`Reader`的子类,从字面意思可以看出它是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。 + +### 构造方法 + +> `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"); +``` + + +### 使用转换流解决编码问题 + +```java +public class ReaderDemo2 { + public static void main(String[] args) throws IOException { + // 定义文件路径,文件为gbk编码 + String FileName = "C:\\A.txt"; + // 创建流对象,默认UTF8编码 + InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName)); + // 创建流对象,指定GBK编码 + InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , "GBK"); + // 定义变量,保存字符 + int read; + // 使用默认编码字符流读取,乱码 + while ((read = isr.read()) != -1) { + System.out.print((char)read); // �����ʺ + } + isr.close(); + + // 使用指定编码字符流读取,正常解析 + while ((read = isr2.read()) != -1) { + System.out.print((char)read); // 沉默王二 + } + isr2.close(); + } +} +``` + + +## 2.4 OutputStreamWriter类--(字符流到字节流的桥梁) + +转换流`java.io.OutputStreamWriter` ,是Writer的子类,字面看容易混淆会误以为是转为字符流,其实不然,OutputStreamWriter为从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。 + +### 构造方法 + +> `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"); +``` + + +### 指定编码构造代码 + +```java +public class OutputDemo { + public static void main(String[] args) throws IOException { + // 定义文件路径 + String FileName = "C:\\s.txt"; + // 创建流对象,默认UTF8编码 + OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName)); + // 写出数据 + osw.write("沉默"); // 保存为6个字节 + osw.close(); + + // 定义文件路径 + String FileName2 = "D:\\A.txt"; + // 创建流对象,指定GBK编码 + OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),"GBK"); + // 写出数据 + osw2.write("王二");// 保存为4个字节 + osw2.close(); + } +} +``` + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/char-byte-61d3a7e6-365e-41d3-8c4a-bc9e680c70a6.png) + + + +为了达到**最高效率**,可以考虑在 `BufferedReader` 内包装 `InputStreamReader` + +```java +BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); +``` + + +>参考链接:[https://www.cnblogs.com/yichunguo/p/11775270.html](https://www.cnblogs.com/yichunguo/p/11775270.html),整理:沉默王二 + + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/io/file-path.md b/docs/io/file-path.md new file mode 100644 index 0000000000..7384ade64e --- /dev/null +++ b/docs/io/file-path.md @@ -0,0 +1,561 @@ +--- +title: 详解 File、Path、Paths、Files 四个类,Java操作文件不再难 +shortTitle: 详解File、Path、Paths、Files +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,详解 File、Path、Paths、Files,操作文件不再难 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java IO,file,paths,files,path +--- + +## File 类 + +至于IO流,也就是输入输出流,从文件出发到文件结束,至始至终都离不开文件,所以IO流还得从文件File类讲起。 + +### File概述 + +`java.io.File` 类是专门对文件进行操作的类,只能对文件本身进行操作,不能对文件内容进行操作。 + +`java.io.File` 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。 + +怎么理解上面两句话?其实很简单! + +第一句就是说File跟流无关,File类不能对文件进行读和写也就是输入和输出! + +第二句就是说File主要表示`D:\\文件目录1`与`D:\\文件目录1\\文件.txt`,前者是文件夹(Directory)后者则是文件(file),而File类就是操作这两者的类。 + +## 构造方法 + +在java中,一切皆是对象,File类也不例外,不论是哪个对象都应该从该对象的构造说起,所以我们来分析分析`File`类的构造方法。首先从API开始着手: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/file-path-37de6cfc-f82f-4459-bc7e-9b4bd7466d70.png) + + +比较常用的构造方法有三个: + +1、 `public File(String pathname)` :通过给定的**路径**来创建新的 File实例。 + +2、 `public File(String parent, String child)` :从**父路径(字符串)和子路径**创建新的 File实例。 + +3、 `public File(File parent, String child)` :从**父路径(File)和子路径名字符串**创建新的 File实例。 + +看文字描述不够生动、不够形象、不得劲?没事,通过举例马上就生动形象了,代码如下: + +```java +1. 一个File对象代表硬盘中实际存在的一个文件或者目录。 +2. File类构造方法不会给你检验这个文件或文件夹是否真实存在,因此无论该路径下是否存在文件或者目录,都不影响File对象的创建。 + +// 文件路径名 +String path = "D:\\123.txt"; +File file1 = new File(path); + +// 文件路径名 +String path2 = "D:\\1\\2.txt"; +File file2 = new File(path2); -------------相当于D:\\1\\2.txt + +// 通过父路径和子路径字符串 + String parent = "F:\\aaa"; + String child = "bbb.txt"; + File file3 = new File(parent, child); --------相当于F:\\aaa\\bbb.txt + +// 通过父级File对象和子路径字符串 +File parentDir = new File("F:\\aaa"); +String child = "bbb.txt"; +File file4 = new File(parentDir, child); --------相当于F:\\aaa\\bbb.txt +``` + + +> File类的注意点: +> +> 1. 一个File对象代表硬盘中实际存在的一个文件或者目录。 +> 2. File类构造方法不会给你检验这个文件或文件夹是否真实存在,因此无论该路径下是否存在文件或者目录,都不影响File对象的创建。 + +### 常用方法 + +File的常用方法主要分为获取功能、获取绝对路径和相对路径、判断功能、创建删除功能的方法 + +#### 获取功能的方法 + +1、`public String getAbsolutePath()` :返回此File的绝对路径。 + +2、`public String getPath()` :结果和getAbsolutePath一致。 + +3、`public String getName()` :返回文件名或目录名。 + +4、`public long length()` :返回文件长度,以字节为单位。 + +测试代码如下【注意测试以自己的电脑文件夹为准】: + +```java +public class FileGet { + public static void main(String[] args) { + File f = new File("d:/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("d:/aaa"); + System.out.println("目录绝对路径:"+f2.getAbsolutePath()); + System.out.println("目录构造路径:"+f2.getPath()); + System.out.println("目录名称:"+f2.getName()); + System.out.println("目录长度:"+f2.length()); + } +} +输出结果: +文件绝对路径:d:\aaa\bbb.java +文件构造路径:d:\aaa\bbb.java +文件名称:bbb.java +文件长度:2116字节 + +目录绝对路径:d:\aaa +目录构造路径:d:\aaa +目录名称:aaa +目录长度:3236 +``` + + +> 注意:`length()` 表示文件的长度,`File`对象表示目录的时候,则返回值无意义。 + +#### 绝对路径和相对路径 + +**绝对路径**:一个完整的路径,以盘符开头,例如`F://aaa.txt`。 + +**相对路径**:一个简化的路径,不以盘符开头,例如`//aaa.txt//b.txt`。 + +> 1、**路径是不区分大小写的** +> +> 2、路径中的文件分隔符,windows使用反斜杠,反斜杠是转义字符,两个反斜杠代表一个普通的反斜杠;macOS 和Linux 使用的是 `/`。 + +```java +//绝对路径 +public class FilePath { + public static void main(String[] args) { + // D盘下的bbb.java文件 + File f = new File("D:\\bbb.java"); + System.out.println(f.getAbsolutePath()); + + // 项目下的bbb.java文件 + File f2 = new File("bbb.java"); + System.out.println(f2.getAbsolutePath()); + } +} +输出结果: +D:\bbb.java +D:\java\bbb.java +``` + + +#### 判断功能的方法 + +1、 `public boolean exists()` :判断文件或目录是否存在。 + +2、 `public boolean isDirectory()` :判断是否为目录。 + +3、`public boolean isFile()` :判断是否为文件。 + +方法演示,代码如下: + +```java +public class FileIs { + public static void main(String[] args) { + File f = new File("d:\\aaa\\bbb.java"); + File f2 = new File("d:\\aaa"); + // 判断是否存在 + System.out.println("d:\\aaa\\bbb.java 是否存在:"+f.exists()); + System.out.println("d:\\aaa 是否存在:"+f2.exists()); + // 判断是文件还是目录 + System.out.println("d:\\aaa 文件?:"+f2.isFile()); + System.out.println("d:\\aaa 目录?:"+f2.isDirectory()); + } +} +输出结果: +d:\aaa\bbb.java 是否存在:true +d:\aaa 是否存在:true +d:\aaa 文件?:false +d:\aaa 目录?:true +``` + + +#### 创建、删除功能的方法 + +* `public boolean createNewFile()` :文件不存在,创建一个新的空文件并返回`true`,文件存在,不创建文件并返回`false`。 +* `public boolean delete()` :删除文件或目录。 +* `public boolean mkdir()` :创建目录。 +* `public boolean mkdirs()` :创建目录,包括任何必需但不存在的父目录。 + +其中,`mkdirs()`和`mkdir()`方法类似,但`mkdir()`,只能创建一级目录,`mkdirs()`可以创建多级目录比如`//a//b//c`,所以**开发中一般用**`mkdirs()`; + +> 这些方法中值得注意的是**createNewFile**方法以及**mkdir**与**mkdirs**的区别 + +方法测试,代码如下: + +```java +public class FileCreateDelete { + public static void main(String[] args) throws IOException { + // 文件的创建 + File f = new File("aaa.txt"); + System.out.println("是否存在:"+f.exists()); // false + System.out.println("是否创建:"+f.createNewFile()); // true + System.out.println("是否创建:"+f.createNewFile()); // 以及创建过了所以再使用createNewFile返回false + System.out.println("是否存在:"+f.exists()); // true + + // 目录的创建 + File f2= new File("newDir"); + System.out.println("是否存在:"+f2.exists());// false + System.out.println("是否创建:"+f2.mkdir()); // true + System.out.println("是否存在:"+f2.exists());// true + + // 创建多级目录 + File f3= new File("newDira\\newDirb"); + System.out.println(f3.mkdir());// false + File f4= new File("newDira\\newDirb"); + System.out.println(f4.mkdirs());// true + + // 文件的删除 + System.out.println(f.delete());// true + + // 目录的删除 + System.out.println(f2.delete());// true + System.out.println(f4.delete());// false + } +} +``` + + +> 注意:`delete`方法,如果此`File`表示目录,则目录必须为空才能删除。 + +### 目录的遍历 + +* `public String[] list()` :返回一个String数组,表示该File目录中的所有子文件或目录。 +* `public File[] listFiles()` :返回一个File数组,表示该File目录中的所有的子文件或目录。 + +```java +public class FileFor { + public static void main(String[] args) { + File dir = new File("G:\光标"); + + //获取当前目录下的文件以及文件夹的名称。 + String[] names = dir.list(); + for(String name : names){ + System.out.println(name); + } + //获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息 + File[] files = dir.listFiles(); + for (File file : files) { + System.out.println(file); + } + } +} +``` + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/file-path-0f1675a7-ee3b-4f28-9cca-00c4ba5a5759.png) + + + +**listFiles**在获取指定目录下的文件或者文件夹时必须满足下面两个条件: + +1,**指定的目录必须存在** + +2,**指定的必须是目录。否则容易引发NullPointerException异常** + +### 递归遍历文件夹下所有文件以及子文件 + +不说啥了,直接上代码: + +```javascript +package File; + +import java.io.File; + +//递归遍历文件夹下所有的文件 +public class RecursionDirectory { + public static void main(String[] args) { + File file=new File("D:\\java专属IO测试"); + Recursion(file); + } + public static void Recursion(File file){ + //1、判断传入的是否是目录 + if(!file.isDirectory()){ + //不是目录直接退出 + return; + } + //已经确保了传入的file是目录 + File[] files = file.listFiles(); + //遍历files + for (File f: files) { + //如果该目录下文件还是个文件夹就再进行递归遍历其子目录 + if(f.isDirectory()){ + //递归 + Recursion(f); + }else { + //如果该目录下文件是个文件,则打印对应的名字 + System.out.println(f.getName()); + } + + } + } +} +``` + +## Path 接口和 Paths 类 + +Path 接口是 NIO2(AIO) 的一部分,是对 NIO 的更新,Path 接口已添加到 Java 7 中,完全限定类名是 java.nio.file.Path 。 + +Path 实例表示文件系统中的路径。 路径可以指向文件或目录,也可以是绝对的或相对的。在某些操作系统中,不要将文件系统路径与环境变量中的 path 路径混淆。 java.nio.file.Path 接口与环境变量path无关。 + +在许多方面,java.nio.file.Path 接口类似于 java.io.File 类,但存在一些细微差别。 不过,在许多情况下,可以使用 Path 接口替换 File 类的使用。 + +### 创建 Path 对象 + +可以使用名为 Paths.get() 创建 Path 实例,get()方法是 Path 实例的工厂方法,一个示例如下: + +```java +public class PathExample { + public static void main(String[] args) { + // 使用绝对路径创建 + Path absolutePath = Paths.get("D:\\test\\1.txt"); + // 使用相对路径创建 + Path relativePath = Paths.get("D:\\test", "1.txt"); + System.out.println(absolutePath.equals(relativePath)); // ture + } +} +``` + + +Paths 类只有2个方法: + +方法|描述| +---|---| +static Path get(String first, String... more)|将路径字符串或在连接时形成路径字符串的字符串序列转换为路径。| +static Path (URI uri)|将给定URI转换为路径对象。| + +Path 接口部分方法: + +方法|描述| +---|---| +boolean endsWith(Path other)|测试此路径是否以给定路径结束。| +boolean equals(Object other)|取决于文件系统的实现。一般不区分大小写,有时区分。 不访问文件系统。| +Path normalize()|返回一个路径,该路径消除了冗余的名称元素,比如'.', '..'| +Path toAbsolutePath()|返回绝对路径。| +File toFile()|转换为 File 对象。| +String toString()|返回的路径字符串。| + +>注意,假如路径为 `D:\\..\\..\\.\\p2\\core\\cache\\binary`,normalize 返回 `D:\\p2\\core\\cache\\binary` + + +## Files 类 + +NIO 文件类(java.nio.file.Files)为操作文件系统中的文件提供了几种方法,File 类与 java.nio.file.Path 类一起工作,需要了解 Path 类,然后才能使用 Files 类。 + +### 判断文件是否存在 + +```java +static boolean exists(Path path, LinkOption... options) +``` + + +options 参数用于指示,在文件是符号链接的情况下,如何处理该符号链接,默认是处理符号链接的。其中 LinkOption 对象是一个枚举类,定义如何处理符号链接的选项。整个类只有一个 `NOFOLLOW_LINKS;` 常量,代表不跟随符号链接。 + +>符号链接(软链接、Symbolic link)是一类特殊的文件, 其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用。一个符号链接文件仅包含有一个文本字符串,其被操作系统解释为一条指向另一个文件或者目录的路径。它是一个独立文件,其存在并不依赖于目标文件。如果删除一个符号链接,它指向的目标文件不受影响。如果目标文件被移动、重命名或者删除,任何指向它的符号链接仍然存在,但是它们将会指向一个不复存在的文件。这种情况被有时被称为被遗弃。 + +### createDirectory(Path path) 创建目录 + +```java +Path output = Paths.get("D:\\test\\output"); +Path newDir = Files.createDirectory(output); +// Files.createDirectories(output); +// 这个方法可以一并创建不存在的父目录 +System.out.println(output == newDir); // true +System.out.println(Files.exists(output)); // true +``` + + +如果创建目录成功,则返回指向新创建的路径的 Path 实例,此实例和参数是同一个实例。 + +如果该目录已存在,则抛出 FileAlreadyExistsException 。 如果出现其他问题,可能会抛出IOException ,例如,如果所需的新目录的父目录不存在。 + +### 复制文件 + +一共有 3 个复制方法: + +```java +static long copy(Path source, OutputStream out); +static Path copy(Path source, Path target, CopyOption... options); +static long copy(InputStream in, Path target, CopyOption... options) +``` + + +其中 CopyOption 选项可以选择指定复制模式,一般是其子枚举类 StandardCopyOption 提供选项,有 3 种模式,第二个参数是可变形参,可以多个组合一起使用: + +1. `ATOMIC_MOVE` :原子复制,不会被线程调度机制打断的操作;一旦开始,就一直运行到结束; +2. `COPY_ATTRIBUTES` :同时复制属性,默认是不复制属性的; +3. `REPLACE_EXISTING` :重写模式,会覆盖已存在的目的文件; + +一个例子如下: + +```java +Path sourcePath = Paths.get("D:\\test\\source.txt"); // 源文件必须先存在 +Path desPath = Paths.get("D:\\test\\des.txt"); // 目的文件可以不存在 +Files.copy(sourcePath, desPath); // 默认情况,如果目的文件已存在则抛出异常 +Files.copy(sourcePath, desPath, StandardCopyOption.REPLACE_EXISTING); // 覆盖模式 +``` + + +注意:复制文件夹的时候,只能复制空文件夹,如果文件夹非空,需要递归复制,否则只能得到一个空文件夹,而文件夹里面的文件不会被复制。 + + +### 移动文件/文件夹 + +只有 1 个移动文件或文件夹的方法: + +```java +static Path move(Path source, Path target, CopyOption... options); +``` + + +如果文件是符号链接,则移动符号链接本身,而不是符号链接指向的实际文件。 + +和移动文件一样,也存在第三个可选参数 CopyOption ,参考上述。如果移动文件失败,可能会抛出 IOException,例如,如果文件已存在于目标路径中,并且遗漏了覆盖选项,或者要移动的源文件不存在等。 + +和复制文件夹不一样,如果文件夹里面有内容,复制只会复制空文件夹,而移动会把文件夹里面的所有东西一起移动过去,以下是一个移动文件夹的示例: + +```java +// 移动 s 目录到一个不存在的新目录 +Path s = Paths.get("D:\\s"); +Path d = Paths.get("D:\\test\\test"); +Files.createDirectories(d.getParent()); +Files.move(s, d); +``` + + +和 Linux mv 命令一样,重命名文件与移动文件方式相同,移动文件还可以将文件移动到不同的目录并可以同时更改其名称。 另外 java.io.File 类也可以使用它的 renameTo() 方法来实现移动文件,但现在 java.nio.file.Files 类中也有文件移动功能。 + +### 删除文件/文件夹 + +```java +static void delete(Path path); +static boolean deleteIfExists(Path path); // 如果文件被此方法删除则返回 true +``` + + +如果文件是目录,则该目录必须为空才能删除。 + +### Files.walkFileTree() 静态方法 + +删除和复制文件夹的时候,如果文件夹为空,那么会删除失败或者只能复制空文件夹,此时可以使用 walkFileTree() 方法进行遍历文件树,然后在 FileVisitor 对象的 visitFile() 方法中执行删除或复制文件操作。 + +Files 类有 2 个重载的 walkFileTree() 方法,如下: + +```java +static Path walkFileTree(Path start, + FileVisitor visitor); + +static Path walkFileTree(Path start, + Set options, + int maxDepth, + FileVisitor visitor); +``` + + +将 Path 实例和 FileVisitor 作为参数,walkfiletree() 方法可以递归遍历目录树。Path 实例指向要遍历的目录。在遍历期间调用 FileVisitor ,首先介绍 FileVisitor 接口: + +```java +public interface FileVisitor { + + FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException; + + FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException; + + FileVisitResult visitFileFailed(T file, IOException exc) throws IOException; + + FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException; +} +``` + + +必须自己实现 FileVisitor 接口,并将其实现的实例传递给 walkFileTree() 方法。在目录遍历期间,将在不同的时间调用 FileVisitor 实现的 4 个方法,代表对遍历到的文件或目录进行什么操作。如果不需要使用到所有方法,可以扩展 SimpleFileVisitor 类,该类包含 FileVisitor 接口中所有方法的默认实现。 + +```java +Files.walkFileTree(inputPath, new FileVisitor() { + // 访问文件夹之前调用此方法 + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + System.out.println("pre visit dir:" + dir); + return FileVisitResult.CONTINUE; + } + + // 访问的每个文件都会调用此方法,只针对文件,不会对目录执行 + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + // 访问文件失败会调用此方法,只针对文件,不会对目录执行 + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + + // 访问文件夹之后会调用此方法 + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } +}); +``` + + +这四个方法都返回一个 FileVisitResult 枚举实例。FileVisitResult 枚举包含以下四个选项: + +* CONTINUE : 继续 +* TERMINATE : 终止 +* SKIP\_SIBLINGS : 跳过兄弟节点,然后继续 +* SKIP\_SUBTREE : 跳过子树(不访问此目录的条目),然后继续,仅在 preVisitDirectory 方法返回时才有意义,除此以外和 CONTINUE 相同。 + +通过返回其中一个值,被调用的方法可以决定文件遍历时接下来应该做什么。 + +### 搜索文件 + +walkFileTree() 方法还可以用于搜索文件,下面这个例子扩展了 SimpleFileVisitor 来查找一个名为 input.txt 的文件: + +```java +Path rootPath = Paths.get("D:\\test"); +String fileToFind = File.separator + "input.txt"; + +Files.walkFileTree(rootPath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String fileString = file.toAbsolutePath().toString(); + System.out.println("pathString: " + fileString); + + if (fileString.endsWith(fileToFind)) { + System.out.println("file found at path: " + fileString); + return FileVisitResult.TERMINATE; + } + return FileVisitResult.CONTINUE; + } +}); +``` + + +同理,删除有内容的目录时,可以重写 visitFile() 方法,并在里面执行删除文件操作,重写 postVisitDirectory() 方法,并在里面执行删除目录操作即可。 + +### Files 类中的其他方法 + +Files 类包含许多其他有用的函数,例如用于创建符号链接,确定文件大小,设置文件权限等的函数。有关java.nio.file.Files 类的详细信息,请查看 [JavaDoc](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html) + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/io/print.md b/docs/io/print.md new file mode 100644 index 0000000000..6da18862a2 --- /dev/null +++ b/docs/io/print.md @@ -0,0 +1,95 @@ +--- +title: Java打印流:PrintStream & PrintWriter +shortTitle: Java打印流PrintStream +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,Java打印流:PrintStream & PrintWriter +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java IO,打印流,PrintStream,PrintWriter +--- + +## 何谓打印流 + +平时我们在控制台打印输出,是调用`print`方法和`println`方法完成的,各位用了这么久的输出语句肯定没想过这两个方法都来自于`java.io.PrintStream`类吧,哈哈。该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。 + +**打印流分类**: + +> 字节打印流PrintStream,字符打印流PrintWriter + +**打印流特点**: + +> A:只操作目的地,不操作数据源 +> +> B:可以操作任意类型的数据 +> +> C:如果启用了自动刷新,在调用println()方法的时候,能够换行并刷新 +> +> D:可以直接操作文件 + +这个时候有同学就要问了,哪些流可以直接操作文件呢?答案很简单,**如果该流的构造方法能够同时接收File和String类型的参数,一般都是可以直接操作文件的**! + +PrintStream是OutputStream的子类,PrintWriter是Writer的子类,两者处于对等的位置上,所以它们的API是非常相似的。二者区别无非一个是字节打印流,一个是字符打印流。 + +## 字节输出打印流PrintStream复制文本文件 + +```java +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintStream; + +public class PrintStreamDemo { + public static void main(String[] args) throws IOException { + BufferedReader br=new BufferedReader(new FileReader("copy.txt")); + PrintStream ps=new PrintStream("printcopy.txt"); + String line; + while((line=br.readLine())!=null) { + ps.println(line); + } + br.close(); + ps.close(); + } +} +``` + + +## 字符输出打印流PrintWriter复制文本文件 + +```java +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +/** + * 使用打印流复制文本文件 + */ +public class PrintWriterDemo { + public static void main(String[] args) throws IOException { + BufferedReader br=new BufferedReader(new FileReader("aa.txt")); + PrintWriter pw=new PrintWriter("printcopyaa.txt"); + String line; + while((line=br.readLine())!=null) { + pw.println(line); + } + br.close(); + pw.close(); + } +} +``` + +>参考链接:[https://www.cnblogs.com/yichunguo/p/11775270.html](https://www.cnblogs.com/yichunguo/p/11775270.html),整理:沉默王二 + + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/io/reader-writer.md b/docs/io/reader-writer.md new file mode 100644 index 0000000000..aba4ed4cd1 --- /dev/null +++ b/docs/io/reader-writer.md @@ -0,0 +1,451 @@ +--- +title: Java字符流Reader和Writer的故事 +shortTitle: 字符流Reader和Writer +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,Java字符流Reader和Writer的故事 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java IO,Reader,Writer,字符流 +--- + +字符流Reader和Writer的故事要从它们的类关系图开始,啥都不说了,直接看图 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/reader-writer-472cf80a-c6d1-4952-b672-15cd54d06eaf.png) + +## 字符流 + +字符流本质是基于字节流读取/写入的,只不过加了编码和解码。如果用字节流直接读取/写入数据会有乱码问题,见下例: + +```java +package IO; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +public class CharaterStream { + public static void main(String[] args) throws Exception { + //FileInputStream为操作文件的字符输入流 + FileInputStream inputStream = new FileInputStream("a.txt");//内容为“沉默王二是傻 X” + + int len; + while ((len=inputStream.read())!=-1){ + System.out.print((char)len); + } + + } +} +运行结果: 沉默王二是傻 X +``` + +具体现状分析: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/reader-writer-0b68ef81-26d0-4a4e-9c1b-61928ce8646c.png) + + +> 字节流读取中文字符时,可能不会显示完整的字符,那是因为一个中文字符占用多个字节。 + +那字节流就没办法了吗?不,字节流依旧有办法,只是麻烦了点,代码如下: + +```java +public class CharaterStream { + public static void main(String[] args) throws Exception { + + FileInputStream inputStream = new FileInputStream("abc.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()`的源码,`String`构造方法有解码功能,并且默认编码是`utf-8`,代码如下: + +```java +public String(byte bytes[], int offset, int length) { + checkBounds(bytes, offset, length); + this.value = StringCoding.decode(bytes, offset, length); +} + +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); + } + try { + return decode("ISO-8859-1", ba, off, len); + } catch (UnsupportedEncodingException x) { + // If this code is hit during VM initialization, MessageUtils is + // the only way we will be able to get any kind of error message. + MessageUtils.err("ISO-8859-1 charset not available: " + + x.toString()); + // If we can not find ISO-8859-1 (a required encoding) then things + // are seriously wrong with the installation. + System.exit(1); + return null; + } +} +public static Charset defaultCharset() { + if (defaultCharset == null) { + synchronized (Charset.class) { + String csn = AccessController.doPrivileged( + new GetPropertyAction("file.encoding")); + Charset cs = lookup(csn); + if (cs != null) + defaultCharset = cs; + else + defaultCharset = forName("UTF-8"); + } + } + return defaultCharset; +} +``` + + +尽管字节流也有办法解决乱码问题,但还是比较麻烦,于是 Java 就有了字符流,以`字符为单位`读写数据,字符流`专门用于处理文本`文件。 + +如果处理纯文本的数据请优先考虑字符流,其他情况就用字节流(图片、视频、等等`只文本`例外)。 + +> 从另一角度来说:**字符流 = 字节流 + 编码表** + +## 1、字符输入流(Reader) + +`java.io.Reader`抽象类是**字符输入流**类的**超类**(父类),可以读取字符信息到内存中。它定义了字符输入流的一些共性方法: + +> 1、`public void close()` :关闭此流并释放与此流相关的系统资源。 +> +> 2、 `public int read()`: 从输入流读取一个字符。 +> +> 3、 `public int read(char[] cbuf)`: 从输入流中读取一些字符,并将它们存储到字符数组 `cbuf`中 + +### FileReader类 + +`java.io.FileReader` 类用来读取字符文件。构造时使用系统默认的字符编码和默认字节缓冲区。 + +#### 构造方法 + +> 1、`FileReader(File file)`: 创建一个新的 FileReader ,参数为**File对象**。 +> +> 2、 `FileReader(String fileName)`: 创建一个新的 FileReader,参数为文件名。 + +构造方法的使用就算不写应该都很熟悉了吧,代码如下: + +```java +public class FileReaderConstructor throws IOException{ + public static void main(String[] args) { + // 使用File对象创建流对象 + File file = new File("a.txt"); + FileReader fr = new FileReader(file); + + // 使用文件名称创建流对象 + FileReader fr = new FileReader("b.txt"); + } +} +``` + + +#### FileReader读取字符数据 + +1. **读取字符**:`read`方法,每次可以读取一个字符的数据,返回读取的字符(转为 int 类型),读取到文件末尾,返回`-1`,循环读取,代码使用演示: + +```java +public class FRRead { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + FileReader fr = new FileReader("abc.txt"); + // 定义变量,保存数据 + int b; + // 循环读取 + while ((b = fr.read())!=-1) { + System.out.println((char)b); + } + // 关闭资源 + fr.close(); + } +} +``` + + +至于读取的写法类似字节流的写法,只是读取单位不同罢了。 + +## 2、字符输出流(Writer) + +`java.io.Writer` 抽象类是**字符输出流**类的**超类**(父类),将指定的字符信息写入到目的地,定义了字符输出流的一些共性方法: + +> 1、`void write(int c)` 写入单个字符。 +> +> 2、`void write(char[] cbuf)` 写入字符数组。 +> +> 3、 `abstract void write(char[] cbuf, int off, int len)` 写入字符数组的一部分,off为开始索引,len为字符个数。 +> +> 4、 `void write(String str)` 写入字符串。 +> +> 5、`void write(String str, int off, int len)` 写入字符串的某一部分,off为字符串的开始索引,len为字符个数。 +> +> 6、`void flush()` 刷新该流的缓冲。 +> +> 7、`void close()` 关闭此流,但要先刷新它。 + +### FileWriter类 + +`java.io.FileWriter` 类用来将字符写入到文件的类。构造时使用系统默认的字符编码和默认字节缓冲区。 + +#### 构造方法 + +1、 `FileWriter(File file)`: 创建一个新的 FileWriter,参数为要读取的File对象。 + +2、`FileWriter(String fileName)`: 创建一个新的 FileWriter,参数为要读取的文件的名称。 + +依旧是熟悉的构造举例,代码如下: + +```java +public class FileWriterConstructor { + public static void main(String[] args) throws IOException { + // 第一种:使用File对象创建流对象 + File file = new File("a.txt"); + FileWriter fw = new FileWriter(file); + + // 第二种:使用文件名称创建流对象 + FileWriter fw = new FileWriter("b.txt"); + } +} +``` + + +#### FileWriter写入数据 + +**写入字符**:`write(int b)` 方法,每次可以写出一个字符数据,代码使用演示: + +```java +public class FWWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + FileWriter fw = new FileWriter("fw.txt"); + // 写出数据 + fw.write(97); // 写出第1个字符 + fw.write('b'); // 写出第2个字符 + fw.write('C'); // 写出第3个字符 + + //关闭资源时,与FileOutputStream不同。 如果不关闭,数据只是保存到缓冲区,并未保存到文件。 + // fw.close(); + } +} +输出结果: +abC +``` + + +> 【注意】**关闭资源时,与FileOutputStream不同。 如果不关闭,数据只是保存到缓冲区,并未保存到文件。** + +#### 关闭close和刷新flush + +因为内置缓冲区的原因,如果不关闭输出流,无法把字符写入到文件中。但是关闭了流对象,就无法继续写数据了。如果我们既想写入数据,又想继续使用流,就需要`flush` 方法了。 + +`flush` :刷新缓冲区,流对象可以继续使用。 + +`close` :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。 + +flush还是比较有趣的,小伙伴们不自己运行一下还真不好体会,来段代码体会体会: + +```java +public class FlushDemo { + public static void main(String[] args) throws Exception { + //源 也就是输入流【读取流】 读取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文件中依旧是空的,并没有任何东西,为啥呢? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/reader-writer-3b4fd024-856f-45ee-8183-1a1ee808e5ce.png) + +【注意】**关闭资源时,FileWriter与FileOutputStream不同,如果不关闭,数据只是保存到缓冲区,并未保存到文件。** + +这个时候反应过来了吧?可见实践的重要性,**编程就是这样,不去敲,永远学不会**!!!所以一定要去敲,多敲啊!!! + +所以,我们在以上的代码中再添加下面三句代码,就完美了,b.txt文件就能复制到源文件的数据了! + + +```java +fr.close(); +fw.flush(); +fw.close(); +``` + + +> `flush()`这个方法是清空缓存的意思,用于清空缓冲区的数据流,进行流的操作时,数据先被读到内存中,然后再把数据写到文件中。 + +那么当你数据读完时,如果这时调用`close()`方法关闭了读写流,就可能造成数据丢失,为什么呢? + +因为,读入数据完成时不代表写入数据完成,一部分数据可能会留在缓存区中,这个时候`flush()`方法就格外重要了。 + +好了,接下来close。使用代码如下: + +```java +public class FWWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + 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方法,释放系统资源。 + +#### FileWriter的续写和换行 + +**续写和换行**:操作类似于[FileOutputStream操作](https://tobebetterjavaer.com/io/stream.html),直接上代码: + +```java +public class FWWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象,可以续写数据 + FileWriter fw = new FileWriter("fw.txt",true); + // 写出字符串 + fw.write("沉默王二"); + // 写出换行 + fw.write("\r\n"); + // 写出字符串 + fw.write("是傻 X"); + // 关闭资源 + fw.close(); + } +} +输出结果: +沉默王二 +是傻 X +``` + + +#### FileReader和FileWriter类完成文本文件复制 + +直接上代码: + +```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("F:\\新建文件夹\\aa.txt");//文件不存在会抛出java.io.FileNotFoundException + //创建输出流对象 + FileWriter fw=new FileWriter("C:\\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(); + } +} +``` + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/reader-writer-0f158401-ee7b-4384-a566-be10bd264fbd.png) + +> 最后再次强调: +> +> 字符流,只能操作文本文件,不能操作图片,视频等非文本文件。当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流 + +## IO异常的处理 + +我们在学习的过程中可能习惯把异常抛出,而实际开发中并不能这样处理,建议使用`try...catch...finally` 代码块,处理异常部分,格式代码如下: + +```java +public class HandleException1 { + public static void main(String[] args) { + // 声明变量 + FileWriter fw = null; + try { + //创建流对象 + fw = new FileWriter("fw.txt"); + // 写出数据 + fw.write("哥敢摸si"); //哥敢摸si + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (fw != null) { + fw.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} +``` + + +好了,到这里,字符流Reader和Writer的故事的到这里了! + + +>参考链接:[https://www.cnblogs.com/yichunguo/p/11775270.html](https://www.cnblogs.com/yichunguo/p/11775270.html),整理:沉默王二 + + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/io/serialize.md b/docs/io/serialize.md new file mode 100644 index 0000000000..166b69166c --- /dev/null +++ b/docs/io/serialize.md @@ -0,0 +1,233 @@ +--- +title: Java序列化流,字节和对象之间的序列化和反序列化 +shortTitle: Java序列化流(序列化和反序列化) +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,Java序列化流,字节和对象之间的序列化和反序列化 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java IO,序列化流,java序列化,java反序列化,ObjectOutputStream,ObjectInputStream +--- + +序列化有什么好处呢?可以把对象写入文本文件或者在网络中传输。 + +如何实现序列化呢?让被序列化的对象所属类实现[Serializbale序列化接口](https://tobebetterjavaer.com/io/Serializbale.html)。 + +接着我们来继续聊序列化和反序列化。 + +## 何谓序列化 + +Java 提供了一种对象**序列化**的机制。用一个字节序列可以表示一个对象,该字节序列包含该`对象的数据`、`对象的类型`和`对象中存储的属性`等信息。字节序列写出到文件之后,相当于文件中**持久保存**了一个对象的信息。 + +反之,该字节序列还可以从文件中读取回来,重构对象,对它进行**反序列化**。`对象的数据`、`对象的类型`和`对象中存储的数据`信息,都可以用来在内存中创建对象。看图理解序列化: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/serialize-8a1b6818-5f58-4057-b521-f8ba670d72a1.png) + +## ObjectOutputStream类 + +`java.io.ObjectOutputStream` 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。 + +### 构造方法 + +`public ObjectOutputStream(OutputStream out)` : 创建一个指定OutputStream的ObjectOutputStream。 + +构造代码如下: + +```java +FileOutputStream fileOut = new FileOutputStream("aa.txt"); +ObjectOutputStream out = new ObjectOutputStream(fileOut); +``` + + +### 序列化操作 + +1. 一个对象要想序列化,必须满足两个条件: + +- 该类必须实现[`java.io.Serializable` 接口](https://tobebetterjavaer.com/io/Serializbale.html),`Serializable` 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出`NotSerializableException` 。 + +- 该类的所有属性都必须是可序列化的。如果有一个属性不需要可序列化,则该属性必须注明是瞬态的,使用[`transient` 关键字](https://tobebetterjavaer.com/io/transient.html)修饰。 + +```java +public class Employee implements java.io.Serializable { + public String name; + public String address; + public transient int age; // transient瞬态修饰成员,不会被序列化 + public void addressCheck() { + System.out.println("Address check : " + name + " -- " + address); + } +} +``` + + +2.写出对象方法 + +`public final void writeObject (Object obj)` : 将指定的对象写出。 + +```java +public class SerializeDemo{ + public static void main(String [] args) { + Employee e = new Employee(); + e.name = "zhangsan"; + e.address = "beiqinglu"; + e.age = 20; + try { + // 创建序列化流对象 + ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt")); + // 写出对象 + out.writeObject(e); + // 释放资源 + out.close(); + fileOut.close(); + System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。 + } catch(IOException i) { + i.printStackTrace(); + } + } +} +输出结果: +Serialized data is saved +``` + + +## ObjectInputStream类 + +ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。 + +### 构造方法 + +`public ObjectInputStream(InputStream in)` : 创建一个指定InputStream的ObjectInputStream。 + +### 反序列化操作1 + +如果能找到一个对象的class文件,我们可以进行反序列化操作,调用`ObjectInputStream`读取对象的方法: + +* `public final Object readObject ()` : 读取一个对象。 + +```java +public class DeserializeDemo { + public static void main(String [] args) { + Employee e = null; + try { + // 创建反序列化流 + FileInputStream fileIn = new FileInputStream("employee.txt"); + ObjectInputStream in = new ObjectInputStream(fileIn); + // 读取一个对象 + e = (Employee) in.readObject(); + // 释放资源 + in.close(); + fileIn.close(); + }catch(IOException i) { + // 捕获其他异常 + i.printStackTrace(); + return; + }catch(ClassNotFoundException c) { + // 捕获类找不到异常 + System.out.println("Employee class not found"); + c.printStackTrace(); + return; + } + // 无异常,直接打印输出 + System.out.println("Name: " + e.name); // zhangsan + System.out.println("Address: " + e.address); // beiqinglu + System.out.println("age: " + e.age); // 0 + } +} +``` + + +**JVM可以反序列化的对象,必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 `ClassNotFoundException` 异常。** + +### 反序列化操作2 + +另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个`InvalidClassException`异常。发生这个异常的原因如下: + +> 1、该类的序列版本号与从流中读取的类描述符的版本号不匹配 +> +> 2、该类包含未知数据类型 +> +> 2、该类没有可访问的无参数构造方法 + +`Serializable` 接口给需要序列化的类,提供了一个序列版本号。`serialVersionUID` 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。 + +```java +public class Employee implements java.io.Serializable { + // 加入序列版本号 + private static final long serialVersionUID = 1L; + public String name; + public String address; + // 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值. + public int eid; + + public void addressCheck() { + System.out.println("Address check : " + name + " -- " + address); + } +} +``` + + +## 序列化集合练习 + +1. 将存有多个自定义对象的集合序列化操作,保存到`list.txt`文件中。 +2. 反序列化`list.txt` ,并遍历集合,打印对象信息。 + +### 案例分析 + +1. 把若干学生对象 ,保存到集合中。 +2. 把集合序列化。 +3. 反序列化读取时,只需要读取一次,转换为集合类型。 +4. 遍历集合,可以打印所有的学生信息 + +### 案例代码实现 + +```java +public class SerTest { + public static void main(String[] args) throws Exception { + // 创建 学生对象 + Student student = new Student("老王", "laow"); + Student student2 = new Student("老张", "laoz"); + Student student3 = new Student("老李", "laol"); + + ArrayList arrayList = new ArrayList<>(); + arrayList.add(student); + arrayList.add(student2); + arrayList.add(student3); + // 序列化操作 + // serializ(arrayList); + + // 反序列化 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt")); + // 读取对象,强转为ArrayList类型 + ArrayList list = (ArrayList)ois.readObject(); + + for (int i = 0; i < list.size(); i++ ){ + Student s = list.get(i); + System.out.println(s.getName()+"--"+ s.getPwd()); + } + } + + private static void serializ(ArrayList arrayList) throws Exception { + // 创建 序列化流 + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt")); + // 写出对象 + oos.writeObject(arrayList); + // 释放资源 + oos.close(); + } +} +``` + + +>参考链接:[https://www.cnblogs.com/yichunguo/p/11775270.html](https://www.cnblogs.com/yichunguo/p/11775270.html),整理:沉默王二 + + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/io/shangtou.md b/docs/io/shangtou.md new file mode 100644 index 0000000000..2930833e7b --- /dev/null +++ b/docs/io/shangtou.md @@ -0,0 +1,401 @@ +--- +title: 看完这篇,Java IO 不再混乱! +shortTitle: 看完这篇,Java IO不再混乱 +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,Java IO 体系看起来类很多,感觉很复杂,但其实是 IO 涉及的因素太多了。在设计 IO 相关的类时,编写者也不是从同一个方面考虑的,所以会给人一种很乱的感觉,并且还有设计模式的使用,更加难以使用这些 IO 类,所以特地对 Java 的 IO 做一个总结。 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java IO,io,输入输出流 +--- + + +“老王,Java IO 也太上头了吧?”新兵蛋子小二向头顶很凉快的老王抱怨道,“你瞧,我就按照传输方式对 IO 进行了一个简单的分类,就能搞出来这么多的玩意!” + +![](http://cdn.tobebetterjavaer.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 $ 这些。 + +通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/shangtou-02.png) + +具体还要看字符编码,比如说在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节。 + + PS:关于字符编码,可以看前面的章节:[锟斤拷](https://tobebetterjavaer.com/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 +private char[] writeBuffer; + +/** + * Size of writeBuffer, must be >= 1 + */ +private static final int WRITE_BUFFER_SIZE = 1024; + +public void write(String str, int off, int len) throws IOException { + synchronized (lock) { + char cbuf[]; + if (len <= WRITE_BUFFER_SIZE) { + if (writeBuffer == null) { + writeBuffer = new char[WRITE_BUFFER_SIZE]; + } + cbuf = writeBuffer; + } else { // Don't permanently allocate very large buffers. + cbuf = new char[len]; + } + str.getChars(off, (off + len), cbuf, 0); + write(cbuf, 0, len); + } +} +``` + +## 02、操作对象划分 + +小二,你细想一下,IO IO,不就是输入输出(Input/Output)嘛: + +- Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等 +- Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。 + +所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。 + +文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。 + +![](http://cdn.tobebetterjavaer.com/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。 + +![](http://cdn.tobebetterjavaer.com/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基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + + + + + + + + + diff --git a/docs/io/stream.md b/docs/io/stream.md new file mode 100644 index 0000000000..3219121afe --- /dev/null +++ b/docs/io/stream.md @@ -0,0 +1,500 @@ +--- +title: 文件的世界,一切皆字节流(Stream) +shortTitle: 文件的世界,一切皆字节流 +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,文件的世界,一切皆字节流 OutputStream、InputStream +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,IO,OutputStream,InputStream,字节流 +--- + +我们必须得明确一点,一切文件数据(文本、图片、视频等)都是以二进制的形式存储的,传输时也如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终是二进制数据。 + +## 字节输出流(OutputStream) + +`java.io.OutputStream` 抽象类是**字节输出流**类的**超类**(父类),它将指定的字节信息写入到目的地,定义了字节输出流的一些共性方法: + +> 1、 `public void close()` :关闭此输出流并释放与此流相关联的系统资源。 +> +> 2、 `public void flush()` :刷新此输出流并强制缓冲区的字节被写入到目的地。 +> +> 3、 `public void write(byte[] b)`:将 b.length个字节从指定的字节数组写入此输出流。 +> +> 4、 `public void write(byte[] b, int off, int len)` :从指定的字节数组写入 len字节到此输出流,从偏移量 off开始。 **也就是说从off个字节数开始一直到len个字节结束** + +> **以上4个方法是字节输出流都具有的方法,由父类OutputStream提供定义,子类共享以上方法** + +### FileOutputStream类 + +`OutputStream`有很多子类,我们从最简单的一个子类FileOutputStream开始。看名字就知道是文件输出流,用于将数据写入到文件。 + +#### FileOutputStream构造方法 + +不管学啥子,只要是对象,就从构造方法开始! + +> 1、 `public FileOutputStream(File file)`:根据文件创建文件输出流对象。 +> +> 2、 `public FileOutputStream(String name)`: 根据文件名创建文件输出流对象。 + +**推荐第二种构造方法**【开发常用】: + +```java +FileOutputStream foutputStream = new FileOutputStream("abc.txt"); +``` + + +就以上面这行代码来讲,类似这样创建字节输出流对象一共做了**三件事情**: + +1、调用系统功能去创建文件【只有输出流对象才会自动创建】 + +2、创建outputStream对象 + +3、把foutputStream对象指向这个文件 + +> 注意: +> +> 创建输出流对象的时候,系统会自动去对应位置创建文件,即使文件不存在也不会报FileNotFoundException异常。 + +当你创建一个流对象时,必须直接或者间接传入一个文件路径。比如现在我们创建一个`FileOutputStream`流对象,在该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。有兴趣的童鞋可以测试一下,具体代码如下: + +```java +public class FileOutputStreamConstructor throws IOException { + public static void main(String[] args) { + // 使用File对象创建流对象 + File file = new File("G:\\自动创建的文件夹\\a.txt"); + FileOutputStream fos = new FileOutputStream(file); + + // 使用文件名称创建流对象 + FileOutputStream fos = new FileOutputStream("G:\\b.txt"); + } +} +``` + + +#### FileOutputStream写入字节数据 + +使用FileOutputStream写入字节数据主要通过`Write`方法,而`write`方法分如下三种 + +```javascript +public void write(int b) +public void write(byte[] b) +public void write(byte[] b,int off,int len) //从`off`索引开始,`len`个字节 +``` + + +1. **写入字节**:`write(int b)` 方法,每次可以写入一个字节数据,代码如下: + +```java +public class IoWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + FileOutputStream fos = new FileOutputStream("fos.txt"); + // 写出数据 + fos.write(97); // 第1个字节 + fos.write(98); // 第2个字节 + fos.write(99); // 第3个字节 + // 关闭资源 + fos.close(); + } +} +输出结果: +abc +``` + +字符 a 的 [ASCII 值](https://tobebetterjavaer.com/basic-extra-meal/java-unicode.html)为 97,字符 b 的ASCII 值为 98,字符 b 的ASCII 值为 99。也就是说,以上代码可以写成: + +```java +public class IoWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + FileOutputStream fos = new FileOutputStream("fos.txt"); + // 写出数据 + fos.write('a'); // 第1个字节 + fos.write('b'); // 第2个字节 + fos.write('c'); // 第3个字节 + // 关闭资源 + fos.close(); + } +} +``` + + +> 1. 虽然参数为int类型,占四个字节,但是这里只会保留一个字节的信息写入(通常来说,一个英文字符是一个字节,一个中文字符是两个字节)。 +> 2. 流操作完毕后,必须释放系统资源,调用close方法,千万记得。 + +2. **写入字节数组**:`write(byte[] b)`,代码使用演示: + +```java +public class FOSWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + FileOutputStream fos = new FileOutputStream("fos.txt"); + // 字符串转换为字节数组 + byte[] b = "麻麻我想吃烤山药".getBytes(); + // 写入字节数组数据 + fos.write(b); + // 关闭资源 + fos.close(); + } +} +输出结果: +麻麻我想吃烤山药 +``` + + +3. **写入指定长度字节数组**:`write(byte[] b, int off, int len)` ,从`off`索引开始,写入`len`个字节,代码如下: + +```java +public class FOSWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + FileOutputStream fos = new FileOutputStream("fos.txt"); + // 字符串转换为字节数组 + byte[] b = "abcde".getBytes(); + // 从索引2开始,2个字节。索引2是c,两个字节,也就是cd。 + fos.write(b,2,2); + // 关闭资源 + fos.close(); + } +} +输出结果: +cd +``` + + +#### FileOutputStream实现数据追加、换行 + +经过以上的代码测试,每次程序运行,都会创建新的输出流对象,于是目标文件中的数据也会被清空。如何保留目标文件中数据,还能继续**追加新数据**呢?以及实现**换行**呢? + +其实很简单。 + +我们先来学习`FileOutputStream`的另外两个构造方法,如下: + +1、`public FileOutputStream(File file, boolean append)` + +2、`public FileOutputStream(String name, boolean append)` + +这两个构造方法,第二个参数中都需要传入一个boolean类型的值,`true` 表示追加数据,`false` 表示不追加也就是清空原有数据。这样创建的输出流对象,就可以指定是否要追加内容了。 + +实现数据追加代码如下: + +```java +public class FOSWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + FileOutputStream fos = new FileOutputStream("fos.txt",true); + // 字符串转换为字节数组 + byte[] b = "abcde".getBytes(); + // 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。 + fos.write(b); + // 关闭资源 + fos.close(); + } +} +文件操作前:cd +文件操作后:cdabcde +``` + + +Windows系统里,换行符号是`\r\n` ,具体代码如下: + +```java +public class FOSWrite { + public static void main(String[] args) throws IOException { + // 使用文件名称创建流对象 + FileOutputStream fos = new FileOutputStream("fos.txt"); + // 定义字节数组 + byte[] words = {97,98,99,100,101}; + // 遍历数组 + for (int i = 0; i < words.length; i++) { + // 写出一个字节 + fos.write(words[i]); + // 写出一个换行, 换行符号转成数组写出 + fos.write("\r\n".getBytes()); + } + // 关闭资源 + fos.close(); + } +} + +输出结果: +a +b +c +d +e +``` + +回车符(CR)和换行符(LF)是文本文件用于标记换行的控制字符(control characters)或字节码(bytecode)。 + +- CR = Carriage Return,回车符号(\r,十六进制 ascii 码为0x0D,十进制 ascii 码为 13),用于将鼠标移动到行首,并不前进至下一行。 +- LF = Line Feed,换行符号( \n, 十六进制 ascii 码为 0x0A,十进制 ascii 码为 10)。 + +紧邻的 CR 和 LF(组成 CRLF,\r\n,或十六进制 0x0D0A)将鼠标移动到下一行行首。(Windows 操作系统默认的文本换行符为 CRLF;Linux 以及 macOS 系统默认使用 LF,早期的 mac os 系统使用 CR 换行。) + +## 字节输入流(InputStream) + +`java.io.InputStream` 抽象类是**字节输入流**类的**超类**(父类),可以读取字节信息到内存中。它定义了字节输入流的一些共性方法: + +> 1、`public void close()` :关闭此输入流并释放与此流相关的系统资源。 +> +> 2、`public abstract int read()`: 从输入流读取数据的下一个字节。 +> +> 3、`public int read(byte[] b)`: 该方法返回的int值代表的是读取了多少个字节,读到几个返回几个,读取不到返回-1 + +#### FileInputStream类 + +`java.io.FileInputStream` 类是文件输入流,从文件中读取字节。 + +#### FileInputStream的构造方法 + +> 1、 `FileInputStream(File file)`: 通过打开与实际文件的连接来创建一个 FileInputStream,该文件由文件系统中的 File 对象 file 命名。 +> +> 2、 `FileInputStream(String name)`: 通过打开与实际文件的连接来创建一个 FileInputStream,该文件由文件系统中的路径名name命名。 + +同样的,推荐使用第二种构造方法: + + +```java +FileInputStream inputStream = new FileInputStream("a.txt"); +``` + + +当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出`FileNotFoundException` 。 + +举例,代码如下: + +```java +public class FileInputStreamConstructor throws IOException{ + public static void main(String[] args) { + // 使用File对象创建流对象 + File file = new File("a.txt"); + FileInputStream fos = new FileInputStream(file); + + // 使用文件名称创建流对象 + FileInputStream fos = new FileInputStream("b.txt"); + } +} +``` + + +#### FileInputStream读取字节数据 + +1. **读取字节**:`read`方法,每次可以读取一个字节的数据,会转为int类型返回,如果读取到文件末尾,则返回`-1`,代码测试如下【read.txt文件中内容为abcde】: + +```java +public class FISRead { + public static void main(String[] args) throws IOException{ + // 使用文件名称创建流对象 + FileInputStream fis = new FileInputStream("read.txt");//read.txt文件中内容为abcde + // 读取数据,返回一个字节 + int read = fis.read(); + System.out.println((char) read); + read = fis.read(); + System.out.println((char) read); + read = fis.read(); + System.out.println((char) read); + read = fis.read(); + System.out.println((char) read); + read = fis.read(); + System.out.println((char) read); + // 读取到末尾,返回-1 + read = fis.read(); + System.out.println( read); + // 关闭资源 + fis.close(); + } +} +输出结果: +a +b +c +d +e +-1 +``` + + +循环改进读取方式,代码使用演示: + +```java +public class FISRead { + public static void main(String[] args) throws IOException{ + // 使用文件名称创建流对象 + FileInputStream fis = new FileInputStream("read.txt"); + // 定义变量,保存数据 + int b; + // 循环读取 + while ((b = fis.read())!=-1) { + System.out.println((char)b); + } + // 关闭资源 + fis.close(); + } +} +输出结果: +a +b +c +d +e +``` + + +2. **使用字节数组读取**:`read(byte[] b)`,每次读取数组b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回`-1` ,代码使用演示: + +```java +public class FISRead { + public static void main(String[] args) throws IOException{ + // 使用文件名称创建流对象. + FileInputStream fis = new FileInputStream("read.txt"); // read.txt文件中内容为abcde + // 定义变量,作为有效个数 + int len; + // 定义字节数组,作为装字节数据的容器 + byte[] b = new byte[2]; + // 循环读取 + while ((len = fis.read(b))!=-1) { + // 每次读取后,把数组变成字符串打印 + System.out.println(new String(b)); + } + // 关闭资源 + fis.close(); + } +} + +输出结果: +ab +cd +ed +``` + +嗯? + +`read.txt`文件中的内容为`abcde`,结果输出了 `abcded`? + +这是由于最后一次读取时,只读取一个字节`e`,而数组中上次读取的数据没有被完全**替换**【注意是替换,看下图】 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/stream-746bf44a-744e-4c47-bc07-af5d845f1852.png) + + + +所以要通过`len`,获取有效的字节。代码如下: + +```java +public class FISRead { + public static void main(String[] args) throws IOException{ + // 使用文件名称创建流对象. + FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde + // 定义变量,作为有效个数 + int len; + // 定义字节数组,作为装字节数据的容器 + byte[] b = new byte[2]; + // 循环读取 + while ((len= fis.read(b))!=-1) { + // 每次读取后,把数组的有效字节部分,变成字符串打印 + System.out.println(new String(b,0,len));// len 每次读取的有效字节个数 + } + // 关闭资源 + fis.close(); + } +} + +输出结果: +ab +cd +e +``` + + +在开发中一般强烈推荐使用数组读取文件,代码如下: + +```java +package io; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class input2 { + public static void main(String args[]){ + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream("a.txt"); + int len = 0 ; + byte[] bys = new byte[1024]; + while ((len = inputStream.read(bys)) != -1) { + System.out.println(new String(bys,0,len)); + } + + } catch (IOException e) { + e.printStackTrace(); + }finally { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } +} +``` + + +#### 字节流FileInputstream复制图片 + +**复制图片原理** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/stream-55e22f8c-8b3f-408d-a12e-e26c0ddbaee6.png) + +**代码实现** + +复制图片文件,代码如下: + +```java +public class Copy { + public static void main(String[] args) throws IOException { + // 1.创建流对象 + // 1.1 指定数据源 + FileInputStream fis = new FileInputStream("D:\\test.jpg"); + // 1.2 指定目的地 + FileOutputStream fos = new FileOutputStream("test_copy.jpg"); + + // 2.读写数据 + // 2.1 定义数组 + byte[] b = new byte[1024]; + // 2.2 定义长度 + int len; + // 2.3 循环读取 + while ((len = fis.read(b))!=-1) { + // 2.4 写出数据 + fos.write(b, 0 , len); + } + + // 3.关闭资源 + fos.close(); + fis.close(); + } +} +``` + + +**注**:**复制文本、图片、mp3、视频等的方式一样**。 + +到这里,字节流OutputStream与InputStream就讲清楚了。 + +>参考链接:[https://www.cnblogs.com/yichunguo/p/11775270.html](https://www.cnblogs.com/yichunguo/p/11775270.html),整理:沉默王二 + + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/io/transient.md b/docs/io/transient.md new file mode 100644 index 0000000000..b80ae39158 --- /dev/null +++ b/docs/io/transient.md @@ -0,0 +1,314 @@ +--- +title: 招银面试官:说说Java transient关键字 +shortTitle: Java transient关键字 +category: + - Java核心 +tag: + - Java IO +description: Java程序员进阶之路,小白的零基础Java教程,Java transient关键字 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,transient +--- + +害,小二最熟的是 Java,但很多 Java 基础知识都不知道,比如 transient 关键字以前就没用到过,所以不知道它的作用是什么,今天去招银面试的时候,面试官问到了这个:说说 Java transient 关键字吧,结果小二直接懵逼了。 + +下面是他自己面试凉了以后回去做的总结,分享出来,大家一起涨下姿势~~~好了,废话不多说,下面开始: + +## 1\. transient 的作用及使用方法 + +我们都知道一个对象只要实现了 Serilizable 接口,这个对象就可以被序列化,java 的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了 Serilizable 接口,这个类的所有属性和方法都会自动序列化。 + +然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上 transient 关键字。 + +换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。 + +总之,java 的 transient 关键字为我们提供了便利,你只需要实现 Serilizable 接口,将不需要序列化的属性前添加关键字 transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。 + +示例 code 如下: + +```java +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * @description 使用transient关键字不序列化某个变量 + * 注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致 + * + * @author Alexia + * @date 2013-10-15 + */ +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("C:/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( + "C:/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: Alexia +password: 123456 read after Serializable: +username: Alexia +password: null +``` + +密码字段为 null,说明反序列化时根本没有从文件中获取到信息。 + +## 2\. transient 使用小结 + +1)一旦变量被 transient 修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。 + +2)transient 关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被 transient 关键字修饰的。变量如果是用户自定义类变量,则该类需要实现 Serializable 接口。 + +3)被 transient 关键字修饰的变量不能被序列化,一个静态变量不管是否被 transient 修饰,均不能被序列化。 + +第三点可能有些人很迷惑,因为发现在 User 类中的 username 字段前加上 static 关键字后,程序运行结果依然不变,即 static 类型的 username 也读出来为“Alexia”了,这不与第三点说的矛盾吗? + +实际上是这样的:第三点确实没错(一个静态变量不管是否被 transient 修饰,均不能被序列化),反序列化后类中 static 型变量 username 的值为当前 JVM 中对应 static 变量的值,这个值是 JVM 中的,不是反序列化得出的,不相信?好吧,下面我来证明: + +```java +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * @description 使用transient关键字不序列化某个变量 + * 注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致 + * + * @author Alexia + * @date 2013-10-15 + */ +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("C:/user.txt")); + os.writeObject(user); // 将User对象写进文件 + os.flush(); + os.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + // 在反序列化之前改变username的值 + User.username = "jmwang"; + + ObjectInputStream is = new ObjectInputStream(new FileInputStream( + "C:/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: Alexia +password: 123456 read after Serializable: +username: jmwang +password: null +``` + +这说明反序列化后类中 static 型变量 username 的值为当前 JVM 中对应 static 变量的值,为修改后 jmwang,而不是序列化时的值 沉默王二。 + +## 3\. transient 使用细节——被 transient 关键字修饰的变量真的不能被序列化吗? + +思考下面的例子: + +```java +import java.io.Externalizable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; + +/** + * @descripiton Externalizable接口的使用 + * + * @author Alexia + * @date 2013-10-15 + * + */ +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(); + } +} +``` + +content 变量会被序列化吗?好吧,我把答案都输出来了,是的,运行结果就是: + +``` +是的,我将会被序列化,不管我是否被transient关键字修饰 +``` + +这是为什么呢,不是说类的变量被 transient 关键字修饰以后将不能序列化了吗? + +我们知道在 Java 中,对象的序列化可以通过实现两种接口来实现,若实现的是 Serializable 接口,则所有的序列化将会自动进行,若实现的是 Externalizable 接口,则没有任何东西可以自动序列化,需要在 writeExternal 方法中进行手工指定所要序列化的变量,这与是否被 transient 修饰无关。 + +因此第二个例子输出的是变量 content 初始化的内容,而不是 null。 + +> 参考链接:[https://www.cnblogs.com/lanxuezaipiao/p/3369962.html](https://www.cnblogs.com/lanxuezaipiao/p/3369962.html),整理:沉默王二 + +--------- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/java8/Lambda.md b/docs/java8/Lambda.md similarity index 84% rename from docs/src/java8/Lambda.md rename to docs/java8/Lambda.md index 30d86efbf7..558e31ad94 100644 --- a/docs/src/java8/Lambda.md +++ b/docs/java8/Lambda.md @@ -1,23 +1,23 @@ --- title: 深入浅出Java 8 Lambda表达式 -shortTitle: 深入浅出Lambda表达式 +shortTitle: Lambda表达式 category: - Java核心 tag: - Java新特性 -description: 本文详细介绍了Java 8引入的Lambda表达式,阐述了Lambda表达式的设计目的和用法。通过实际的代码示例,展示了如何使用Lambda表达式来简化代码,提高编程效率。学习本文,让您快速掌握Java 8 Lambda表达式的使用技巧,享受函数式编程带来的编程乐趣。 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,深入浅出Java 8 Lambda表达式 head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java8,lambda,java lambda,Lambda表达式, 函数式编程 + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java8,lambda --- 今天分享的主题是《Lambda 表达式入门》,这也是之前一些读者留言强烈要求我写一写的,不好意思,让你们久等了,现在来满足你们,为时不晚吧? -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-1.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/Lambda-1.jpg) -### 01、初识 Lambda +## 01、初识 Lambda Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行。考虑下面这段代码: @@ -72,9 +72,9 @@ public class LamadaTest { 是不是很妙!比起匿名内部类,Lambda 表达式不仅易于理解,更大大简化了必须编写的代码数量。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-2.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/Lambda-2.jpg) -### 02、Lambda 语法 +## 02、Lambda 语法 每个 Lambda 表达式都遵循以下法则: @@ -140,8 +140,7 @@ public static void main(String[] args) { 和匿名内部类一样,不要在 Lambda 表达式主体内对方法内的局部变量进行修改,否则编译也不会通过:Lambda 表达式中使用的变量必须是 final 的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-3.jpg) - +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/Lambda-3.jpg) 这个问题发生的原因是因为 Java 规范中是这样规定的: >Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression @@ -182,7 +181,7 @@ c = 2; 下面我们来详细地一一介绍下。 -#### 01)把 limit 变量声明为 static +### 01)把 limit 变量声明为 static 要想把 limit 变量声明为 static,就必须将 limit 变量放在 `main()` 方法外部,因为 `main()` 方法本身是 static 的。完整的代码示例如下所示。 @@ -213,7 +212,7 @@ public class ModifyVariable2StaticInsideLambda { OK,该方案是可行的。 -#### 02)把 limit 变量声明为 AtomicInteger +### 02)把 limit 变量声明为 AtomicInteger AtomicInteger 可以确保 int 值的修改是原子性的,可以使用 `set()` 方法设置一个新的 int 值,`get()` 方法获取当前的 int 值。 @@ -244,7 +243,7 @@ public class ModifyVariable2AtomicInsideLambda { OK,该方案也是可行的。 -#### 03)使用数组 +### 03)使用数组 使用数组的方式略带一些欺骗的性质,在声明数组的时候设置为 final,但更改 int 的值时却修改的是数组的一个元素。 @@ -275,7 +274,7 @@ public class ModifyVariable2ArrayInsideLambda { OK,该方案也是可行的。 -### 03、Lambda 和 this 关键字 +## 03、Lambda 和 this 关键字 Lambda 表达式并不会引入新的作用域,这一点和匿名内部类是不同的。也就是说,Lambda 表达式主体内使用的 this 关键字和其所在的类实例相同。 @@ -345,9 +344,9 @@ this = com.cmower.java_demo.journal.LamadaTest@3feba861 符合我们分析的预期。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-4.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/Lambda-4.jpg) -### 04、最后 +## 04、最后 尽管 Lambda 表达式在简化 Java 编程方面做了很多令人惊讶的努力,但在某些情况下,不当的使用仍然会导致不必要的混乱,大家伙慎用。 @@ -355,9 +354,8 @@ this = com.cmower.java_demo.journal.LamadaTest@3feba861 ---- -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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/java8/optional.md b/docs/java8/optional.md similarity index 85% rename from docs/src/java8/optional.md rename to docs/java8/optional.md index 13bdbed986..e14f555b32 100644 --- a/docs/src/java8/optional.md +++ b/docs/java8/optional.md @@ -1,26 +1,26 @@ --- -title: Java 8 Optional最佳指南,优雅解决空指针 +title: Java 8 Optional最佳指南 shortTitle: Optional最佳指南 category: - Java核心 tag: - Java新特性 -description: 本文详细介绍了Java 8引入的Optional类,阐述了Optional的设计初衷和用法。通过实际的代码示例,展示了如何使用Optional来优雅地解决空指针问题,避免程序中的NullPointerException。掌握Optional的使用方法,让您的Java代码更加健壮和可靠。 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java 8 Optional最佳指南 head: - - meta - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java8,Optional,java Optional,空指针异常, NullPointerException + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java8,Optional --- -想学习,永远都不晚,尤其是针对 Java 8 里面的好东西,Optional 就是其中之一,该类提供了一种用于表示可选值而非空引用的类级别解决方案。作为一名 Java 程序员,我真的是烦透了 [NullPointerException(NPE)](https://javabetter.cn/exception/npe.html),尽管和它熟得就像一位老朋友,知道它也是迫不得已——程序正在使用一个对象却发现这个对象的值为 null,于是 Java 虚拟机就怒发冲冠地把它抛了出来当做替罪羊。 +想学习,永远都不晚,尤其是针对 Java 8 里面的好东西,Optional 就是其中之一,该类提供了一种用于表示可选值而非空引用的类级别解决方案。作为一名 Java 程序员,我真的是烦透了 NullPointerException(NPE),尽管和它熟得就像一位老朋友,知道它也是迫不得已——程序正在使用一个对象却发现这个对象的值为 null,于是 Java 虚拟机就怒发冲冠地把它抛了出来当做替罪羊。 当然了,我们程序员是富有责任心的,不会坐视不管,于是就有了大量的 null 值检查。尽管有时候这种检查完全没有必要,但我们已经习惯了例行公事。终于,Java 8 看不下去了,就引入了 Optional,以便我们编写的代码不再那么刻薄呆板。 -![](https://cdn.paicoding.com/stutymore/guava-20230329172935.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/optional-1.jpg) -### 01、没有 Optional 会有什么问题 +## 01、没有 Optional 会有什么问题 我们来模拟一个实际的应用场景。小王第一天上班,领导老马就给他安排了一个任务,要他从数据库中根据会员 ID 拉取一个会员的姓名,然后将姓名打印到控制台。虽然是新来的,但这个任务难不倒小王,于是他花了 10 分钟写下了这段代码: @@ -59,7 +59,7 @@ Exception in thread "main" java.lang.NullPointerException at com.cmower.dzone.optional.WithoutOptionalDemo.main(WithoutOptionalDemo.java:24) ``` -### 02、Optional 是如何解决这个问题的 +## 02、Optional 是如何解决这个问题的 小王把代码提交后,就兴高采烈地去找老马要新的任务了。本着虚心学习的态度,小王请求老马看一下自己的代码,于是老王就告诉他应该尝试一下 Optional,可以避免没有必要的 null 值检查。现在,让我们来看看小王是如何通过 Optional 来解决上述问题的。 @@ -95,7 +95,7 @@ class Member { Optional 之所以可以解决 NPE 的问题,是因为它明确的告诉我们,不需要对它进行判空。它就好像十字路口的路标,明确地告诉你该往哪走。 -### 03、创建 Optional 对象 +## 03、创建 Optional 对象 1)可以使用静态方法 `empty()` 创建一个空的 Optional 对象 @@ -128,7 +128,7 @@ System.out.println(optOrNull); // 输出:Optional.empty `ofNullable()` 方法内部有一个三元表达式,如果为参数为 null,则返回私有常量 EMPTY;否则使用 new 关键字创建了一个新的 Optional 对象——不会再抛出 NPE 异常了。 -### 04、判断值是否存在 +## 04、判断值是否存在 可以通过方法 `isPresent()` 判断一个 Optional 对象是否存在,如果存在,该方法返回 true,否则返回 false——取代了 `obj != null` 的判断。 @@ -137,20 +137,20 @@ Optional opt = Optional.of("沉默王二"); System.out.println(opt.isPresent()); // 输出:true Optional optOrNull = Optional.ofNullable(null); -System.out.println(optOrNull.isPresent()); // 输出:false +System.out.println(opt.isPresent()); // 输出:false ``` Java 11 后还可以通过方法 `isEmpty()` 判断与 `isPresent()` 相反的结果。 ```java Optional opt = Optional.of("沉默王二"); -System.out.println(opt.isEmpty()); // 输出:false +System.out.println(opt.isPresent()); // 输出:false Optional optOrNull = Optional.ofNullable(null); -System.out.println(optOrNull.isEmpty()); // 输出:true +System.out.println(opt.isPresent()); // 输出:true ``` -### 05、非空表达式 +## 05、非空表达式 Optional 类有一个非常现代化的方法——`ifPresent()`,允许我们使用函数式编程的方式执行一些代码,因此,我把它称为非空表达式。如果没有该方法的话,我们通常需要先通过 `isPresent()` 方法对 Optional 对象进行判空后再执行相应的代码: @@ -175,11 +175,11 @@ Optional opt = Optional.of("沉默王二"); opt.ifPresentOrElse(str -> System.out.println(str.length()), () -> System.out.println("为空")); ``` -### 06、设置(获取)默认值 +## 06、设置(获取)默认值 有时候,我们在创建(获取) Optional 对象的时候,需要一个默认值,`orElse()` 和 `orElseGet()` 方法就派上用场了。 -`orElse()` 方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值的类型一致。 +`orElse()` 方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值得类型一致。 ```java String nullName = null; @@ -254,7 +254,7 @@ orElseGet 咦,`orElseGet()` 没有去调用 `getDefaultValue()`。哪个方法的性能更佳,你明白了吧? -### 07、获取值 +## 07、获取值 直观从语义上来看,`get()` 方法才是最正宗的获取 Optional 对象值的方法,但很遗憾,该方法是有缺陷的,因为假如 Optional 对象的值为 null,该方法会抛出 NoSuchElementException 异常。这完全与我们使用 Optional 类的初衷相悖。 @@ -278,7 +278,7 @@ Exception in thread "main" java.util.NoSuchElementException: No value present 尽管抛出的异常是 NoSuchElementException 而不是 NPE,但在我们看来,显然是在“五十步笑百步”。建议 `orElseGet()` 方法获取 Optional 对象的值。 -### 08、过滤值 +## 08、过滤值 小王通过 Optional 类对之前的代码进行了升级,完成后又兴高采烈地跑去找老马要任务了。老马觉得这小伙子不错,头脑灵活,又干活积极,很值得培养,就又交给了小王一个新的任务:用户注册时对密码的长度进行检查。 @@ -311,7 +311,7 @@ System.out.println(result); 这次程序输出的结果为 true,因为密码变成了 7 位,在 6 到 10 位之间。想象一下,假如小王使用 if-else 来完成这个任务,代码该有多冗长。 -### 09、转换值 +## 09、转换值 小王检查完了密码的长度,仍然觉得不够尽兴,觉得要对密码的强度也进行检查,比如说密码不能是“password”,这样的密码太弱了。于是他又开始研究起了 `map()` 方法,该方法可以按照一定的规则将原有 Optional 对象转换为一个新的 Optional 对象,原有的 Optional 对象不会更改。 @@ -350,15 +350,18 @@ public class OptionalMapFilterDemo { } ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/optional-2.jpg) + + + + +![](http://cdn.tobebetterjavaer.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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/java8/stream.md b/docs/java8/stream.md new file mode 100644 index 0000000000..83bd0eb888 --- /dev/null +++ b/docs/java8/stream.md @@ -0,0 +1,324 @@ +--- +title: Java 8 Stream流详细用法 +shortTitle: Stream流详细用法 +category: + - Java核心 +tag: + - Java新特性 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java 8 Stream流详细用法 +head: + - - meta + - name: keywords + content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,java8,stream +--- + +两个星期以前,就有读者强烈要求我写一篇 Java Stream 流的文章,我说市面上不是已经有很多了吗,结果你猜他怎么说:“就想看你写的啊!”你看你看,多么苍白的喜欢啊。那就“勉为其难”写一篇吧,嘻嘻。 + +![](http://cdn.tobebetterjavaer.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] +[周杰伦, 王力宏, 陶喆, 林俊杰] +周杰伦, 王力宏, 陶喆, 林俊杰 +``` + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/stream-2.jpg) + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/jvm/asm.md b/docs/jvm/asm.md similarity index 90% rename from docs/src/jvm/asm.md rename to docs/jvm/asm.md index f722e6ae90..4d5648dc1d 100644 --- a/docs/src/jvm/asm.md +++ b/docs/jvm/asm.md @@ -5,13 +5,14 @@ category: - Java核心 tag: - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,史上最通俗易懂的ASM教程 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,史上最通俗易懂的ASM教程 head: - - meta - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,asm + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,asm --- +# 史上最通俗易懂的ASM教程 ## 一勺思想 @@ -32,7 +33,7 @@ ASM是一种通用Java字节码操作和分析框架。它可以用于修改现 我们编写的java文件,会通过javac命令编译为class文件,JVM最终会执行该类型文件来运行程序。下图所示为class文件结构。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-43844b78-c01f-4990-b038-3c91ff2eeb34.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-43844b78-c01f-4990-b038-3c91ff2eeb34.jpg) 下面我们通过一个简单的实例来进行说明。下面是我们编写的一个简单的java文件,只是简单的函数调用. @@ -191,7 +192,7 @@ SourceFile: "Test.java" JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性。在线程中执行一个方法时,我们会创建一个栈帧入栈并执行,如果该方法又调用另一个方法时会再次创建新的栈帧然后入栈,方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧,随后虚拟机将会丢弃此栈帧。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-e31b7e50-1d48-4eef-9552-6fa7e6c68fed.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-e31b7e50-1d48-4eef-9552-6fa7e6c68fed.jpg) ### 局部变量表 @@ -243,9 +244,9 @@ public int sub(int, int); a = b + c 的字节码执行过程中操作数栈以及局部变量表的变化如下图所示 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-4670450e-6199-4562-9cf4-354234c734c8.jpg) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-9808d639-327f-4796-80d4-1809be0b9106.jpg) ## ASM操作 @@ -346,7 +347,7 @@ mv.visitEnd(); 可以一键生成对应的ASM API代码 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-3c8c8db4-5b6a-4576-b147-62965d0e0c1c.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-3c8c8db4-5b6a-4576-b147-62965d0e0c1c.jpg) ---- @@ -354,9 +355,8 @@ mv.visitEnd(); ---- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/bytecode.md b/docs/jvm/bytecode.md new file mode 100644 index 0000000000..f828efcb80 --- /dev/null +++ b/docs/jvm/bytecode.md @@ -0,0 +1,468 @@ +--- +title: 从javap的角度轻松看懂字节码 +shortTitle: 从javap的角度轻松看懂字节码 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,从javap的角度轻松看懂字节码 +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,字节码,javap +--- + +# 从javap的角度轻松看懂字节码 + + +### 01、字节码 + +计算机比较“傻”,只认 0 和 1,这意味着我们编写的代码最终都要编译成机器码才能被计算机执行。Java 在诞生之初就提出了一个非常著名的宣传口号: "**一次编写,处处运行**"。 + +> **Write Once, Run Anywhere.** + +为了这个口号,Java 的亲妈 Sun 公司以及其他虚拟机提供商发布了许多可以在不同平台上运行的 Java 虚拟机,而这些虚拟机都拥有一个共同的功能,那就是可以载入和执行同一种与平台无关的字节码(Byte Code)。 + +有了 Java 虚拟机的帮助,我们编写的 Java 源代码不必再根据不同平台编译成对应的机器码了,只需要生成一份字节码,然后再将字节码文件交由运行在不同平台上的 Java 虚拟机读取后执行就可以了。 + +如今的 Java 虚拟机非常强大,不仅支持 Java 语言,还支持很多其他的编程语言,比如说 Groovy、Scala、Koltin 等等。 + +![](http://cdn.tobebetterjavaer.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 文件(我用的是 Intellij IDEA,在 macOS 环境下)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-bd941085-ff0e-4abf-a5f9-afb0493bfed7.png) + + + +对于这些 16 进制内容,除了开头的 cafe babe,剩下的内容大致可以翻译成:啥玩意啊这...... + +同学们别慌,就从"cafe babe"说起吧,这 4 个字节称之为**魔数**,也就是说,只有以"cafe babe"开头的 class 文件才能被 Java 虚拟机接受,这 4 个字节就是字节码文件的身份标识。 + +目光右移,0000 是 Java 的次版本号,0037 转化为十进制是 55,是主版本号,Java 的版本号从 45 开始,每升一个大版本,版本号加 1,大家可以启动福尔摩斯模式,推理一下。 + +再往后面就是字符串常量池。《[class 文件](https://mp.weixin.qq.com/s/uMEZ2Xwctx4n-_8zvtDp5A)》那一篇我是顺着十六进制内容往下分析的,可能初学者看起来比较头大,这次我们换一种更容易懂的方式。 + +### **02、反编译字节码文件** + +Java 内置了一个反编译命令 javap,可以通过 `javap -help` 了解 javap 的基本用法。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-84b7af5c-93b1-4f63-bb30-946ab3d7e98c.png) + + +OK,我们输入命令 `javap -v -p Main.class` 来查看一下输出的内容。 + +``` +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 行: + +``` +Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class +``` + +字节码文件的位置。 + +第 2 行: + +``` +Last modified 2021年4月15日; size 385 bytes +``` + +字节码文件的修改日期、文件大小。 + +第 3 行: + +``` +SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c +``` + +字节码文件的 SHA-256 值。 + +第 4 行: + +``` +Compiled from "Main.java" +``` + + 说明该字节码文件编译自 Main.java 源文件。 + +第 5 行: + +``` +public class com.itwanger.jvm.Main +``` + +字节码文件的类全名。 + +第 6 行 `minor version: 0`,次版本号。 + +第 7 行 `major version: 55`,主版本号。 + +第 8 行: + +``` +flags: (0x0021) ACC_PUBLIC, ACC_SUPER +``` + +类访问标记,一共有 8 种。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-d12d6983-f427-40d2-bb4b-3a2c6c4c7806.png) + +表明当前类是 `ACC_PUBLIC | ACC_SUPER`。位运算符 `|` 的意思是如果相对应位是 0,则结果为 0,否则为 1,所以 `0x0001 | 0x0020` 的结果是 `0x0021`(需要转成二进制进行运算)。 + +第 9 行: + +``` +this_class: #3 // com/itwanger/jvm/Main +``` + +当前类的索引,指向常量池中下标为 3 的常量,可以看得出当前类是 Main 类。 + +第 10 行: + +``` +super_class: #4 // java/lang/Object +``` + +父类的索引,指向常量池中下标为 6 的常量,可以看得出当前类的父类是 Object 类。 + +第 11 行: + +``` +interfaces: 0, fields: 1, methods: 2, attributes: 1 +``` + +当前类有 0 个接口,1 个字段(age),2 个方法(`write()`方法和缺省的默认构造方法),1 个属性(该类仅有的一个属性是 SourceFIle,包含了源码文件的信息)。 + +### 03、常量池 + +接下来是 Constant pool,也就是字节码文件最重要的常量池部分。可以把常量池理解为字节码文件中的资源仓库,主要存放两大类信息。 + +1)字面量(Literal),有点类似 Java 中的常量概念,比如文本字符串,final 常量等。 + +2)符号引用(Symbolic References),属于编译原理方面的概念,包括 3 种: + +- 类和接口的全限定名(Fully Qualified Name) +- 字段的名称和描述符(Descriptor) +- 方法的名称和描述符 + +Java 虚拟机是在加载字节码文件的时候才进行的动态链接,也就是说,字段和方法的符号引用只有经过运行期转换后才能获得真正的内存地址。当 Java 虚拟机运行时,需要从常量池获取对应的符号引用,然后在类创建或者运行时解析并翻译到具体的内存地址上。 + +当前字节码文件中一共有 21 个常量,它们之间是有链接的,逐个分析会比较乱,我们采用顺藤摸瓜的方式,从上依次往下看,那些被链接的常量我们就点到为止。 + +*注*: + +- `#` 号后面跟的是索引,索引没有从 0 开始而是从 1 开始,是因为设计者考虑到,“如果要表达不引用任何一个常量的含义时,可以将索引值设为 0 来表示”(《深入理解 Java 虚拟机》描述的)。 + +- `=` 号后面跟的是常量的类型,没有包含前缀 `CONSTANT_` 和后缀 `_info`。 + +- 全文中提到的索引等同于下标,为了灵活描述,没有做统一。 + +--- + +第 1 个常量: + +``` +#1 = Methodref #4.#18 // java/lang/Object."":()V +``` + +类型为 Methodref,表明是用来定义方法的,指向常量池中下标为 4 和 18 的常量。 + +第 4 个常量: + +``` +#4 = Class #21 // java/lang/Object +``` + +类型为 Class,表明是用来定义类(或者接口)的,指向常量池中下标为 21 的常量。 + +第 21 个常量: + +``` +#21 = Utf8 java/lang/Object +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 `java/lang/Object`。 + +第 18 个常量: + +``` +#18 = NameAndType #7:#8 // "":()V +``` + +类型为 NameAndType,表明是字段或者方法的部分符号引用,指向常量池中下标为 7 和 8 的常量。 + +第 7 个常量: + +``` +#7 = Utf8 +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 ``,表明为构造方法。 + +第 8 个常量: + +``` +#8 = Utf8 ()V +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 `()V`,表明方法的返回值为 void。 + +到此为止,第 1 个常量算是摸完了。组合起来的意思就是,Main 类使用的是默认的构造方法,来源于 Object 类。 + +---- + +第 2 个常量: + +``` +#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I +``` + +类型为 Fieldref,表明是用来定义字段的,指向常量池中下标为 3 和 19 的常量。 + +第 3 个常量: + +``` +#3 = Class #20 // com/itwanger/jvm/Main +``` + +类型为 Class,表明是用来定义类(或者接口)的,指向常量池中下标为 20 的常量。 + +第 19 个常量: + +``` +#19 = NameAndType #5:#6 // age:I +``` + +类型为 NameAndType,表明是字段或者方法的部分符号引用,指向常量池中下标为 5 和 6 的常量。 + +第 5 个常量: + +``` +#5 = Utf8 age +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 `age`,表明字段名为 age。 + +第 6 个常量: + +``` +#6 = Utf8 I +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 `I`,表明字段的类型为 int。 + +关于字段类型的描述符映射表如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-cbf16ce9-7853-4050-a1c0-8b874f3b0c1e.png) + +到此为止,第 2 个常量算是摸完了。组合起来的意思就是,声明了一个类型为 int 的字段 age。 + +---- + +### 04、字段表集合 + +字段表用来描述接口或者类中声明的变量,包括类变量和成员变量,但不包含声明在方法中局部变量。 + +字段的修饰符一般有: + +- 访问权限修饰符,比如 public private protected +- 静态变量修饰符,比如 static +- final 修饰符 +- 并发可见性修饰符,比如 volatile +- 序列化修饰符,比如 transient + +然后是字段的类型(可以是基本数据类型、数组和对象)和名称。 + +在 Main.class 字节码文件中,字段表的信息如下所示。 + +``` +private int age; + descriptor: I + flags: (0x0002) ACC_PRIVATE +``` + +表明字段的访问权限修饰符为 private,类型为 int,名称为 age。 + +字段的访问标志和类的访问标志非常类似。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-5f328e11-3486-4eb4-8fa9-5c5febfab894.png) + + + + +### **05、方法表集合** + +方法表用来描述接口或者类中声明的方法,包括类方法和成员方法,以及构造方法。方法的修饰符和字段略有不同,比如说 volatile 和 transient 不能用来修饰方法,再比如说方法的修饰符多了 synchronized、native、strictfp 和 abstract。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-fd434d5c-ffc6-4a24-9787-98e573035068.png) + +下面这部分为构造方法,返回类型为 void,访问标志为 public。 + +``` + public com.itwanger.jvm.Main(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC +``` + +来详细看一下其中 Code 属性。 + +``` + 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 虚拟机在运行的时候会根据这个值来分配栈帧的操作数栈深度。 + +- locals 为局部变量所需要的存储空间,单位为槽(slot),方法的参数变量和方法内的局部变量都会存储在局部变量表中。 + +- args_size 为方法的参数个数。 + +为什么 stack 的值为 2,locals 的值为 1,args_size 的值为 1 呢? 默认的构造方法不是没有参数和局部变量吗? + +这是因为有一个隐藏的 this 变量,只要不是静态方法,都会有一个当前类的对象 this 悄悄的存在着。这就解释了为什么 locals 和 args_size 的值为 1 的问题。那为什么 stack 的值为 2 呢?因为字节码指令 invokespecial(调用父类的构造方法进行初始化)会消耗掉一个当前类的引用,所以 aload_0 执行了 2 次,也就意味着操作数栈的大小为 2。 + +关于字节码指令,我们后面再详细介绍。 + +- LineNumberTable,该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。 + +- LocalVariableTable,该属性的作用是描述帧栈中的局部变量与源码中定义的变量之间的关系。大家仔细看一下,就能看到 this 的影子了。 + +下面这部分为成员方法 `getAge()`,返回类型为 int,访问标志为 public。 + +``` + public int getAge(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC +``` + +理解了构造方法的 Code 属性后,再看 `getAge()` 方法的 Code 属性时,就很容易理解了。 + +``` + 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。 + +------- + + +其实学习是这样的,可以横向扩展,也可以纵向扩展。当我们初学编程的时候,特别想多学一点,属于横向扩展,当有了一定的编程经验后,想更上一层楼,就需要纵向扩展,不断深入地学,连根拔起,从而形成自己的知识体系。 + +无论是从十六进制的字节码角度,还是 jclasslib 图形化查看反编译后的字节码的角度,也或者是今天这样从 javap 反编译后的角度,都能窥探出一些新的内容来! + +初学者一开始接触字节码的时候会感觉比较头大,没关系,我当初也是这样,随着时间的推移,经验的积累,慢慢就好了,越往深处钻,就越能体会到那种“技术我有,雄霸天下”的感觉~ + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/class-file-jiegou.md b/docs/jvm/class-file-jiegou.md new file mode 100644 index 0000000000..f65c40cc1d --- /dev/null +++ b/docs/jvm/class-file-jiegou.md @@ -0,0 +1,375 @@ +--- +title: 详解Java的类文件(class文件)结构 +shortTitle: 详解class文件结构 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,详解Java的类文件(class文件)结构 +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,class +--- + +# 详解Java的类文件(class文件)结构 + + +大家好,我是二哥呀,今天我拿了一把小刀,准备解剖一下 Java 的 class 文件。 + +CS 的世界里流行着这么一句话,“计算机科学领域的任何问题都可以通过增加一个中间层来解决”。对于 Java 来说,JVM 就是这么一个产物,“Write once, Run anywhere”之所以能实现,靠得就是 JVM,它能在不同的操作系统下运行同一份源代码编译后的 class 文件。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-dfd7ce0d-1da2-4547-b2d7-57e0350f5911.png) + +Java 是跨平台的,JVM 作为中间层,自然要针对不同的操作系统提供不同的实现。拿 JDK 11 来说,它的实现就有上图中提到的这么多种。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-b1386f9e-c69b-44b0-a8d0-69ffbe9ed31f.png) + +通过不同操作系统的 JVM,我们的源代码就可以不用根据不同的操作系统编译成不同的二进制可执行文件了,跨平台的目标也就实现了。那这个 class 文件到底是什么玩意呢?它是怎么被 JVM 识别的呢? + +我们用 IDEA 编写一段简单的 Java 代码,文件名为 Hello.java。 + +```java +package com.itwanger.jvm; +class Hello { + public static void main(String[] args) { + System.out.println("Hello!"); + } +} +``` + +点击编译按钮后,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!"); + } +} +``` + +看起来和源代码很像,只是多了一个空的构造方法,对吧?它是 class 文件被 IDEA 自带的反编译工具 Fernflower 反编译后的样子。那真实的 class 文件长什么样子呢? + +可以在 terminal 面板下用 `xxd Hello.class` 命令来查看。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-cb4afe63-6a8e-4ae1-a822-d4163c814daa.png) + +咦?完全看不懂的样子呢。它是 class 文件的一种十六进制形式,`xxd` 这个命令的神奇之处就是它能将一个给定文件转换成十六进制形式。 + +### 01、魔数 + +第一行中有一串特殊的字符 `cafebabe`,它就是一个魔数,是 JVM 识别 class 文件的标志,JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果不是则会抛出 `ClassFormatError`。 + +魔数 `cafebabe` 的中文意思显而易见,咖啡宝贝,再加上 Java 的图标本来就是一个热气腾腾的咖啡,可见 Java 与咖啡的渊源有多深。 + +### 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 月份推出。 + +### 03、常量池 + +紧跟在版本号之后的是常量池,字符串常量和较大的证书都会存储在常量池中,当使用这些数值时,会根据常量池中的索引来查找。 + +Java 定义了 boolean、byte、short、char 和 int 等基本数据类型,它们在常量池中都会被当做 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 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.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。 + +对于 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 个字节,但没有直观的感受,现在有了。 + +![](http://cdn.tobebetterjavaer.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 文件中的位置。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-801ed589-658c-407e-ac64-81fd525d7324.png) + +前面还有 3 个字节,第一个字节 0x01 是标识,标识类型为 *CONSTANT_Uft8_info*,第二个和第三个自己 0x00 0x05 用来表示第三部分字节数组的长度。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-ae4f38c9-68fe-40ad-91c6-3e7fd360de05.png) + +与 *CONSTANT_Uft8_info* 类型对应的,还有一个 *CONSTANT_String_info*,用来表示字符串对象(之前代码中的 s),标识是 0x08。前者存储了字符串真正的值,后者并不包含字符串的内容,仅仅包含了一个指向常量池中 *CONSTANT_Uft8_info* 的索引。来看一下它在 class 文件中的位置。 + + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-4e093bef-d592-4be7-847e-0ef5900c5fa4.png) + +*CONSTANT_String_info* 通过索引 19 来找到 *CONSTANT_Uft8_info*。 + +![](http://cdn.tobebetterjavaer.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,可以直接在 IDEA 的插件市场安装。安装完成后,选中 class 文件,然后在 View 菜单里找到 Show Bytecode With Jclasslib 子菜单,就可以查看 class 文件的关键信息了。 + +![](http://cdn.tobebetterjavaer.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 文件中的位置了。 + +![](http://cdn.tobebetterjavaer.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* 包含的索引有两个。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-70cd8902-136c-42a4-ab57-d6baf202e462.png) + +一个是 4,一个是 5,可以通过下图来表示 *CONSTANT_NameAndType_info* 的构成。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-5ac7d4c4-b905-462c-90f7-58b46fc5dda1.png) + +对应 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.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 语言的符号表(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*,我就不再一一说明了,大家也可以拿把小刀去试一试。 + +啊,class 文件中最复杂的常量池部分就算是解剖完了,真不容易! + +### 04、访问标记 + +紧跟着常量池之后的区域就是访问标记(Access flags),这个标记用于识别类或接口的访问信息,比如说到底是 class 还是 interface?是 public 吗?是 abstract 抽象类吗?是 final 类吗?等等。总共有 16 个标记位可供使用,但常用的只有其中 7 个。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-1f5d3154-9a28-4cfa-935e-43d7e023036e.png) + +来看一个简单的枚举代码。 + +```java +public enum Color { + RED,GREEN,BLUE; +} +``` + +通过 jclasslib 可以看到访问标记的信息有 `0x4031 [public final enum]`。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-d4873db5-1a9d-4e05-9765-59a71b083fe5.png) + +对应 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-774e8289-582b-4762-9dce-b0590ee5ad3f.png) + +### 05、this_class、super_class、interfaces + +这三部分用来确定类的继承关系,this_class 为当前类的索引,super_class 为父类的索引,interfaces 为接口。 + +来看下面这段简单的代码,没有接口,默认继承 Object 类。 + +```java +class Hello { + public static void main(String[] args) { + + } +} +``` + +通过 jclasslib 可以看到类的继承关系。 + +![](http://cdn.tobebetterjavaer.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 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-6d5d9189-12a2-45d3-b811-92deede2f78d.png) + +### 06、字段表 + +一个类中定义的字段会被存储在字段表(fields)中,包括静态的和非静态的。 + +来看这样一段代码。 + +```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 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-5a40ed62-4ff2-4101-b2d5-15760032f563.png) + +### 07、方法表 + +方法表和字段表类似,区别是用来存储方法的信息,包括方法名,方法的参数,方法的签名。 + +就拿 main 方法来说吧。 + +```java +public class MethodsTest { + public static void main(String[] args) { + + } +} +``` + +先用 jclasslib 看一下大概的信息。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-cbe6d025-84a5-4fea-821b-a4234f47c6cd.png) + +- 访问标记是 public static 的。 +- 方法名为 main。 +- 方法的参数为字符串数组;返回类型为 Void。 + +对应到 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.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 的静态变量。先通过 jclasslib 看一下它当中一个很重要的属性——ConstantValue,用来表示静态变量的初始值。 + +![](http://cdn.tobebetterjavaer.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*。 + +我画了一副图,可以完整的表示字段的结构,包含属性表在内。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-53f73e24-f060-45d2-8e29-34263c31847b.png) + +对应到 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.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, 方法的关键信息都存储在里面。 + +![](http://cdn.tobebetterjavaer.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 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-b5853549-b17b-48eb-8eb3-a393fb5d655f.png) + +到此为止,class 文件的内部算是剖析得差不多了,希望能对大家有所帮助。第一次拿刀,手有点颤,如果哪里有不足的地方,欢迎大家在评论区毫不留情地指出来! + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/class-load.md b/docs/jvm/class-load.md new file mode 100644 index 0000000000..4ce175844b --- /dev/null +++ b/docs/jvm/class-load.md @@ -0,0 +1,199 @@ +--- +title: 我竟然不再抗拒Java的类加载机制了 +shortTitle: 我竟然不再抗拒Java的类加载机制了 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,我竟然不再抗拒Java的类加载机制了 +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,类加载机制 +--- + +# 我竟然不再抗拒Java的类加载机制了 + + +### 01、字节码 + +在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。 + +计算机只认识 0 和 1,所以任何语言编写的程序都需要编译成机器码才能被计算机理解,然后执行,Java 也不例外。 + +Java 在诞生的时候喊出了一个非常牛逼的口号:“Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多可以在不同平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-load-01.png) + + +到底 Java 字节码是什么样子,我们借助一段简单的代码来看一看。 + +源码如下: + +```java +package com.cmower.java_demo; + +public class Test { + + public static void main(String[] args) { + System.out.println("沉默王二"); + } + +} +``` + +代码编译通过后,通过 `xxd Test.class` 命令查看一下这个字节码文件。 + +``` +xxd Test.class +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 文件的标志。文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 `8950 4e47 `。 + +至于其他内容嘛,可以选择忘记了。 + +### 02、类加载过程 + +了解了 Java 字节码后,我们来聊聊 Java 的类加载过程。 + +Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 + +1)Loading(载入) + +JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 `java.lang.Class` 对象。 + +2)Verification(验证) + +JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。 + +- 确保二进制字节流格式符合预期(比如说是否以 `cafe bene` 开头)。 +- 是否所有方法都遵守访问控制关键字的限定。 +- 方法调用的参数个数和类型是否正确。 +- 确保变量在使用之前被正确初始化了。 +- 检查变量是否被赋予恰当类型的值。 + +3)Preparation(准备) + +JVM 会在该阶段对类变量(也称为静态变量,`static` 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。 + +也就是说,假如有这样一段代码: + +```java +public String chenmo = "沉默"; +public static String wanger = "王二"; +public static final String cmower = "沉默王二"; +``` + +chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 `null`。 + +需要注意的是,`static final` 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 cmower 在准备阶段的值为“沉默王二”而不是 `null`。 + +4)Resolution(解析) + +该阶段将常量池中的符号引用转化为直接引用。 + +what?符号引用,直接引用? + +**符号引用**以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。 + +在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 `com.Wanger` 类引用了 `com.Chenmo` 类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,因此只能使用符号 `com.Chenmo`。 + +**直接引用**通过对符号引用进行解析,找到引用的实际内存地址。 + +5)Initialization(初始化) + +该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。 + +oh,no,上面这段话说得很抽象,不好理解,对不对,我来举个例子。 + +```java +String cmower = new String("沉默王二"); +``` + +上面这段代码使用了 `new` 关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 cmower 进行实例化。 + +### 03、类加载器 + +聊完类加载过程,就不得不聊聊类加载器。 + +一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试 + `ClassNotFoundException` 和 `NoClassDefFoundError` 等异常。 + +对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 `equals`)。 + +站在程序员的角度来看,Java 类加载器可以分为三种。 + +1)启动类加载器(Bootstrap Class-Loader),加载 `jre/lib` 包下面的 jar 文件,比如说常见的 rt.jar。 + +2)扩展类加载器(Extension or Ext Class-Loader),加载 `jre/lib/ext` 包下面的 jar 文件。 + +3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。 + +来来来,通过一段简单的代码了解下。 + +```java +public class Test { + + public static void main(String[] args) { + ClassLoader loader = Test.class.getClassLoader(); + while (loader != null) { + System.out.println(loader.toString()); + loader = loader.getParent(); + } + } + +} +``` + +每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 `类名.class.getClassLoader()` 可以获取到此引用;然后通过 `loader.getParent()` 可以获取类加载器的上层类加载器。 + +这段代码的输出结果如下: + +``` +sun.misc.Launcher$AppClassLoader@73d16e93 +sun.misc.Launcher$ExtClassLoader@15db9742 +``` + +第一行输出为 Test 的类加载器,即应用类加载器,它是 `sun.misc.Launcher$AppClassLoader` 类的实例;第二行输出为扩展类加载器,是 `sun.misc.Launcher$ExtClassLoader` 类的实例。那启动类加载器呢? + +按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 `getParent()` 返回 `null`。所以没有输出。 + + +### 04、双亲委派模型 + +如果以上三种类加载器不能满足要求的话,程序员还可以自定义类加载器(继承 `java.lang.ClassLoader` 类),它们之间的层级关系如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-load-02.png) + +这种层次关系被称作为**双亲委派模型**:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。 + +PS:双亲委派模型突然让我联想到朱元璋同志,这个同志当上了皇帝之后连宰相都不要了,所有的事情都亲力亲为,只有自己没精力没时间做的事才交给大臣们去干。 + +使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。 + +上文中曾提到,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。 + +### 05、最后 + +硬着头皮翻看了大量的资料,并且动手去研究以后,我发现自己竟然对 Java 类加载机制(JVM 将类的信息动态添加到内存并使用的一种机制)不那么抗拒了——真是蛮奇妙的一件事啊。 + +也许学习就应该是这样,只要你敢于挑战自己,就能收获知识——就像山就在那里,只要你肯攀登,就能到达山顶。 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/compile-jdk.md b/docs/jvm/compile-jdk.md new file mode 100644 index 0000000000..9e37b12e2f --- /dev/null +++ b/docs/jvm/compile-jdk.md @@ -0,0 +1,371 @@ +--- +title: 自己编译JDK +shortTitle: 自己编译JDK +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,自己编译JDK +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,JDK +--- + +# 自己编译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`写的,所以相关编译器也跑不掉。 + +一图胜千言,各平台上的编译器支持如下表所示,按平台选择即可: + +![](http://cdn.tobebetterjavaer.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`命令可用。 + +![](http://cdn.tobebetterjavaer.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/` + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-1bbbb1f8-da01-46e1-a793-487a25193c68.png) + + +选择你想要的版本下载即可。 + +* * * + +### 编译前的自动配置 + +源码包下载好,放到本地某个目录(建议路径纯英文,避免不必要的麻烦),解压之,然后进入源码根目录,执行: + +`sh configure +` + +> 当然这里运行的是默认配置项。 + +这一步会进行一系列的自动配置工作,时间一般很快,最终如果能出现一下提示,那么很幸运,编译前的配置工作就完成了! + +这里我给出我自己分别在配置`JDK 11`和`JDK 8`时候完成时的样子: + +**配置JDK 8完成:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-27593edc-03e2-4a42-baf3-ed5e5096b3cb.png) + + +**配置JDK 11完成:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-8526d944-a36e-4d37-93a0-9ad4ad53f927.png) + + +**注:** 如果这一步出错,大概率是某个软件环境未装,或者即使装了,但版本不匹配,控制台打印日志里一般是会提醒的。 + +比如我在配置`JDK 8`的时候,就遇到了一个`errof:GCC compiler is required`的问题: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-2957399f-6451-46dc-a003-76e5159265e9.png) + + +明明系统里已经有编译器,但还是报这个错误。通过后来修改 `jdk源码根目录/common/autoconf/generated-configure.sh`文件,将相关的两行代码注释后就配置通过了 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-ffa10d36-3a77-48aa-ae0c-d3daf67f9a19.png) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-a6f6e416-639e-4706-8b40-6152eb3cf85d.png) + + +配置完成,接下来开始执行真正的编译动作了! + +* * * + +## 真正的编译动作 + +我们这里进行的是全量编译,直接在我们下载的`JDK`源码根目录下执行如下命令即可: + +`make all +` + +这一步编译需要一点时间,耐心等待一下即可。编译过程如果有错误,会终止编译,如果能看到如下两个画面,那么则恭喜你,自己编译`JDK`源码就已经通过了,可以搞一杯咖啡庆祝一下了。 + +**JDK 8编译完成:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-89020f5a-0909-4c57-8c88-f655293a42a4.png) + + +**JDK 11编译完成:** + +![](http://cdn.tobebetterjavaer.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`命令验证: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-f02dff40-f27e-476c-998b-bd6cdb5d3559.png) + + +其次,编译生成的成品`JDK`套装,可以在目录 + +`jdk源码根目录/build/macosx-x86_64-normal-server-release/images +` + +下找到,如图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-1c781d34-776e-4acc-8d2b-b34bc59fda61.png) + + +其中: + +* `j2sdk-image`:编译生成的JDK +* `j2re-image`:编译生成的JRE + +进入`j2sdk-image`目录会发现,里面的内容和我们平时从网络上下载的成品`JDK`内容一致。 + +![](http://cdn.tobebetterjavaer.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`命令验证: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-f9b55425-f308-44e8-8812-ac59b2707c81.png) + + +其次,编译生成的成品`JDK 11`套装,可以在目录 + +`JDK源码根目录/build/macosx-x86_64-normal-server-release/images +` + +下找到,如图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-4e96858f-f681-4498-b1c4-282d317a6a32.png) + + +其中`jdk`目录就是编译生成的成品`JDK 11`套装。 + +* * * + +## 使用自己编译的JDK + +既然我们已经动手编译出了`JDK`成品,接下来我们得用上哇。 + +新建一个最最基本的`Java`工程,比如命名为`JdkTest`,目的是把我们自己编译出的`JDK`给用上。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-2cf54b29-9b7e-46b2-8cde-4c36960aa09b.png) + + +我们点开`Project Structure`,选到`SDKs`选项,新添加上自己刚刚编译生成的JDK,并选为项目的JDK,看看是否能正常工作 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-ad8023d0-fbb7-48b1-856e-a8818677a0a5.png) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-b8d87f09-6178-44c7-9572-a2852e81318d.png) + + +点击确定之后,我们运行之: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-3164cf31-8078-46d7-bee0-22e05b0c08de.png) + + +可以看到我们自己编译出的JDK已经用上了。 + +* * * + +## 关联JDK源码并修改 + +我们继续在上一步`JdkTest`项目的`Project Structure` → `SDKs`里将`JDK`源码关联到自行下载的JDK源码路径上: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-129ede68-c368-461e-92d6-38a8e5dee344.png) + + +这样方便我们对自己下载的`JDK源码`进行**阅读**、**调试**、**修改**、以及在源码里随意**做笔记**和**加注释**。 + +举个最简单的例子,比如我们打开`System.out.println()`这个函数的底层源码: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-c406b2a2-208a-4a54-a869-b3f526e93ccd.png) + + +我们随便给它修改一下,加两行简单的标记,像这样: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-2a46b215-04e5-458a-b475-4dc31d7fe326.png) + + +为了使我们新加的代码行生效,我们必须要重新去JDK源码的根目录中再次执行 `make images`重新编译生成JDK方可生效: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-fd3cf88d-007e-4615-99b5-a3499c35ef40.png) + + +因为之前已经全量编译过了,所以再次`make`的时候增量编译一般很快。 + +重新编译之后,我们再次运行`JdkTest`项目,就可以看到改动的效果了: + +![](http://cdn.tobebetterjavaer.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()`源码为例,我们添加几行中文注释: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-d6a44833-b908-4824-8862-1679bfdddfa3.png) + + +这时候我们去JDK源码目录下编译会发现满屏类似这样的报错: + +> 错误: 编码 ascii 的不可映射字符 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-ad0ca5a3-36c7-477d-bd58-d6731a87d762.png) + + +顿时有点懵,毕竟仅仅是加了几行注释。对于我们来说,源码里写点多行的中文注释基本是**刚需**,然而编译竟会报错,这还能不能让人愉快的玩耍了... 当时后背有点发凉。 + +实不相瞒,就这个问题排查了一段时间,熬到了很晚。最终折腾了一番,通过如下这种方式解决了,顺便分享给小伙伴们,大家如果遇到了这个问题,可以参考着解决一下。 + +因为从控制台的报错可以很明显的看出,肯定是字符编码相关的问题导致的,而且都指向了`ascii`这种编码方式。 + +于是将JDK的源码从根目录导入了Vs Code,然后全目录查找`encoding ascii`相关的内容,看看有没有什么端倪,结果发现 + +`jdk源码根目录/make/common/SetupJavaCompilers.gmk`文件中有两处指定了`ascii`相关的编码方式: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-a86933af-f6d5-4d45-b4ca-33069a212c52.png) + + +于是尝试将这两处`-encoding ascii`的均替换成`-encoding utf-8`: + +![](http://cdn.tobebetterjavaer.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) + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/jvm/cpu-percent-100.md b/docs/jvm/cpu-percent-100.md new file mode 100644 index 0000000000..956fc8d439 --- /dev/null +++ b/docs/jvm/cpu-percent-100.md @@ -0,0 +1,168 @@ +--- +title: 一次生产CPU 100%排查优化实践 +shortTitle: 一次生产CPU 100%排查优化实践 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,一次生产CPU 100% 排查优化实践 +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,cpu +--- + +# 一次生产CPU 100% 排查优化实践 + +## 前言 + +最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题。 + + +## 定位问题 + +拿到问题后首先去服务器上看了看,发现运行的只有我们的 Java 应用。于是先用 `ps` 命令拿到了应用的 `PID`。 + +接着使用 `top -Hp pid` 将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-e9b35104-fce9-40ea-ae91-8bbb7fd8aa96.jpg) + +果然某些线程的 CPU 使用率非常高。 + + +为了方便定位问题我立马使用 `jstack pid > pid.log` 将线程栈 `dump` 到日志文件中。 + +我在上面 100% 的线程中随机选了一个 `pid=194283` 转换为 16 进制(2f6eb)后在线程快照中查询: + +> 因为线程快照中线程 ID 都是16进制存放。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-f8b051d5-f28d-481e-a0b2-e97151797e3b.jpg) + +发现这是 `Disruptor` 的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 [OOM]():[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) + +没想到又来一出。 + +为了更加直观的查看线程的状态信息,我将快照信息上传到专门分析的平台上。 + +[http://fastthread.io/](http://fastthread.io/) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-d6c9bc1c-9600-47f2-9ff1-d0c9bd8ef849.jpg) + +其中有一项菜单展示了所有消耗 CPU 的线程,我仔细看了下发现几乎都是和上面的堆栈一样。 + +也就是说都是 `Disruptor` 队列的堆栈,同时都在执行 `java.lang.Thread.yield` 函数。 + +众所周知 `yield` 函数会让当前线程让出 `CPU` 资源,再让其他线程来竞争。 + +根据刚才的线程快照发现处于 `RUNNABLE` 状态并且都在执行 `yield` 函数的线程大概有 30几个。 + +因此初步判断为大量线程执行 `yield` 函数之后互相竞争导致 CPU 使用率增高,而通过对堆栈发现是和使用 `Disruptor` 有关。 + +## 解决问题 + +而后我查看了代码,发现是根据每一个业务场景在内部都会使用 2 个 `Disruptor` 队列来解耦。 + +假设现在有 7 个业务类型,那就等于是创建 `2*7=14` 个 `Disruptor` 队列,同时每个队列有一个消费者,也就是总共有 14 个消费者(生产环境更多)。 + +同时发现配置的消费等待策略为 `YieldingWaitStrategy` 这种等待策略确实会执行 yield 来让出 CPU。 + +代码如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-49840c0d-2c10-4bcb-80c6-1df7553ddb6c.jpg) + +> 初步看来和这个等待策略有很大的关系。 + +### 本地模拟 + +为了验证,我在本地创建了 15 个 `Disruptor` 队列同时结合监控观察 CPU 的使用情况。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-7f3b2fa6-6505-4b67-9f42-0170a236832b.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-d597089d-54e0-49ef-a0f9-41798e84de48.jpg) + +创建了 15 个 `Disruptor` 队列,同时每个队列都用线程池来往 `Disruptor队列` 里面发送 100W 条数据。 + +消费程序仅仅只是打印一下。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-97b88b4d-2d81-47ab-9beb-830ac122c282.jpg) + +跑了一段时间发现 CPU 使用率确实很高。 + +--- + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-c0ee1da2-29af-4581-b0d8-97f6250401e7.jpg) + +同时 `dump` 线程发现和生产的现象也是一致的:消费线程都处于 `RUNNABLE` 状态,同时都在执行 `yield`。 + +通过查询 `Disruptor` 官方文档发现: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-de904a90-8b59-4333-82f5-9ec94a6525a0.jpg) + +> YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用`自旋 + yield`的方式来提高性能。 +> 当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 + +--- + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-3faf6f7e-0d2c-4cfe-8e3a-07e15601485d.jpg) + +同时查阅到其他的等待策略 `BlockingWaitStrategy` (也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。 + +于是在和之前同样的条件下将等待策略换为 `BlockingWaitStrategy`。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-12912ce3-a702-4bb2-a19b-816c22f7d43a.jpg) + +--- + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-b4aad83e-af9d-48fc-bcd0-ad2a42588179.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-56dc1513-8f10-422f-bb2a-ae5dcfb8413f.jpg) + +和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。 + + +### 优化解决 + +看样子将等待策略换为 `BlockingWaitStrategy` 可以减缓 CPU 的使用, + +但留意到官方对 `YieldingWaitStrategy` 的描述里谈道: +当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 + +而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 `Disruptor` 队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 `YieldingWaitStrategy`)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-b1cbc2c2-828a-46e8-ba14-86cd0fa660c6.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-f8fb7682-a61a-407d-923c-890a16bce109.jpg) + +跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。 + +## 总结 + +所以排查到此可以有一个结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 `Disruptor` 队列。 + +由于是在一台服务器上运行,所以 CPU 资源都是共享的,这就会导致 CPU 的使用率居高不下。 + +所以我们的调整方式如下: + +- 为了快速缓解这个问题,先将等待策略换为 `BlockingWaitStrategy`,可以有效降低 CPU 的使用率(业务上也还能接受)。 +- 第二步就需要将应用拆分(上文模拟的一个 `Disruptor` 队列),一个应用处理一种业务类型;然后分别单独部署,这样也可以互相隔离互不影响。 + +当然还有其他的一些优化,因为这也是一个老系统了,这次 dump 线程居然发现创建了 800+ 的线程。 + +创建线程池的方式也是核心线程数、最大线程数是一样的,导致一些空闲的线程也得不到回收;这样会有很多无意义的资源消耗。 + +所以也会结合业务将创建线程池的方式调整一下,将线程数降下来,尽量的物尽其用。 + + +本文的演示代码已上传至 GitHub: + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor) + +**你的点赞与分享是对我最大的支持** + +原文链接:[https://github.com/crossoverJie/JCSprout/blob/master/docs/jvm/cpu-percent-100.md](https://github.com/crossoverJie/JCSprout/blob/master/docs/jvm/cpu-percent-100.md) + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/gc.md b/docs/jvm/gc.md new file mode 100644 index 0000000000..1110dc3f4a --- /dev/null +++ b/docs/jvm/gc.md @@ -0,0 +1,246 @@ +--- +title: 咱们从头到尾说一次Java垃圾回收 +shortTitle: 咱们从头到尾说一次Java垃圾回收 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,咱们从头到尾说一次Java垃圾回收 +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,垃圾回收,gc +--- + +# 咱们从头到尾说一次Java垃圾回收 + + +之前上学的时候有这个一个梗,说在食堂里吃饭,吃完把餐盘端走清理的,是 C++ 程序员,吃完直接就走的,是 Java 程序员。 + +确实,在 Java 的世界里,似乎我们不用对垃圾回收那么的专注,很多初学者不懂 GC,也依然能写出一个能用甚至还不错的程序或系统。但其实这并不代表 Java 的 GC 就不重要。相反,它是那么的重要和复杂,以至于出了问题,那些初学者除了打开 GC 日志,看着一堆0101的天文,啥也做不了。 + +今天我们就从头到尾完整地聊一聊 Java 的垃圾回收。 + +### 什么是垃圾回收 + +* 垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。 +* Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的 allocated,然后不停的析构。于是,有人就提出,能不能写一段程序实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢? +* 1960年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时 Java 还没有出世呢!所以实际上 GC 并不是Java的专利,GC 的历史远远大于 Java 的历史! + +### 怎么定义垃圾 + +既然我们要做垃圾回收,首先我们得搞清楚垃圾的定义是什么,哪些内存是需要回收的。 + +**引用计数算法** +引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收。 + +**String m = new String("jack");** + +先创建一个字符串,这时候"jack"有一个引用,就是 m。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-691109d2-bee4-4a79-8da6-87c5fd233f54.jpg) + +然后将 m 设置为 null,这时候"jack"的引用次数就等于0了,在引用计数算法中,意味着这块内容就需要被回收了。 + +**m = null;** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-74865618-4576-4f8b-baf3-17d6a71125b9.jpg) + +引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。 + +看似很美好,但我们知道JVM的垃圾回收就是"Stop-The-World"的,那是什么原因导致我们最终放弃了引用计数算法呢?看下面的例子。 + +```java +public class ReferenceCountingGC { + +public Object instance; + +public ReferenceCountingGC(String name){} +} + +public static void testGC(){ + +ReferenceCountingGC a = new ReferenceCountingGC("objA"); +ReferenceCountingGC b = new ReferenceCountingGC("objB"); + +a.instance = b; +b.instance = a; + +a = null; +b = null; +} +``` + +**1\. 定义2个对象** +**2\. 相互引用** +**3\. 置空各自的声明引用** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-fe980c00-3605-4b5d-a711-7edbfd2c80b0.jpg) + +我们可以看到,最后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。 + +**可达性分析算法** + +可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-1636ce77-77b3-4b10-b75a-c0c2d28912c5.jpg) + +通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于 GC Root。 + +**Java 内存区域** + +在 Java 语言中,可作为 GC Root 的对象包括以下4种: + +* 虚拟机栈(栈帧中的本地变量表)中引用的对象 +* 方法区中类静态属性引用的对象 +* 方法区中常量引用的对象 +* 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-6abf9f50-dc53-4e8f-a7f6-3e74df8803d6.jpg) + +1、虚拟机栈(栈帧中的本地变量表)中引用的对象 +此时的 s,即为 GC Root,当s置空时,localParameter 对象也断掉了与 GC Root 的引用链,将被回收。 + +```java +public class StackLocalParameter { +public StackLocalParameter(String name){} +} + +public static void testGC(){ +StackLocalParameter s = new StackLocalParameter("localParameter"); +s = null; +} +``` + +2、方法区中类静态属性引用的对象 +s 为 GC Root,s 置为 null,经过 GC 后,s 所指向的 properties 对象由于无法与 GC Root 建立关系被回收。 + +而 m 作为类的静态属性,也属于 GC Root,parameter 对象依然与 GC root 建立着连接,所以此时 parameter 对象并不会被回收。 + +```java +public class MethodAreaStaicProperties { +public static MethodAreaStaicProperties m; +public MethodAreaStaicProperties(String name){} +} + +public static void testGC(){ +MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties"); +s.m = new MethodAreaStaicProperties("parameter"); +s = null; +} +``` + +3、方法区中常量引用的对象 +m 即为方法区中的常量引用,也为 GC Root,s 置为 null 后,final 对象也不会因没有与 GC Root 建立联系而被回收。 + +```java +public class MethodAreaStaicProperties { +public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final"); +public MethodAreaStaicProperties(String name){} +} + +public static void testGC(){ +MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties"); +s = null; +} +``` + +4、本地方法栈中引用的对象 +任何 native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-a138a4b4-56cb-4d6f-a65a-7f4259977476.jpg) + +### 怎么回收垃圾 + +在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,这里我们讨论几种常见的垃圾收集算法的核心思想。 + +**标记 --- 清除算法** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-2001e224-0f34-4429-bc89-a8fbe8ab271c.jpg) + +标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。就像上图一样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。 + +这逻辑再清晰不过了,并且也很好操作,但它存在一个很大的问题,那就是内存碎片。 + +上图中等方块的假设是 2M,小一些的是 1M,大一些的是 4M。等我们回收完,内存就会切成了很多段。我们知道开辟内存空间时,需要的是连续的内存区域,这时候我们需要一个 2M的内存区域,其中有2个 1M 是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。 + +**复制算法** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-a2b15e6f-6921-4710-bf76-77858df38c27.jpg) + +复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。 + +上面的图很清楚,也很明显的暴露了另一个问题,合着我这140平的大三房,只能当70平米的小两房来使?代价实在太高。 + +**标记整理算法** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-2d47a225-ad9d-4f15-9b4d-7dce9a693adf.jpg) + +标记整理算法(Mark-Compact)标记过程仍然与标记 --- 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。 + +标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。 + +分代收集算法分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记 --- 整理算法来进行回收。so,另一个问题来了,那内存区域到底被分为哪几块,每一块又有什么特别适合什么算法呢? + +### 内存模型与回收策略 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-59dddea1-b6bc-4fd4-bb79-d81adbdc7bed.jpg) + +Java 堆(Java Heap)是JVM所管理的内存中最大的一块,堆又是垃圾收集器管理的主要区域,这里我们主要分析一下 Java 堆的结构。 + +Java 堆主要分为2个区域-年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。可能这时候大家会有疑问,为什么需要 Survivor 区,为什么Survivor 还要分2个区。不着急,我们从头到尾,看看对象到底是怎么来的,而它又是怎么没的。 + +**Eden 区** + +IBM 公司的专业研究表明,有将近98%的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。 + +通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。 + +**Survivor 区** + +Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。 + +1、为啥需要? + +不就是新生代到老年代么,直接 Eden 到 Old 不好了吗,为啥要这么复杂。想想如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。 + +所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。 + +2、为啥需要俩? + +设置两个 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 也支持进行特殊设置。 + +3、动态对象年龄 + +虚拟机并不重视要求对象年龄必须到15岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的综合大于 Survivor 空间的一般,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你“成年”。 + +这其实有点类似于负载均衡,轮询是负载均衡的一种,保证每台机器都分得同样的请求。看似很均衡,但每台机的硬件不通,健康状况不同,我们还可以基于每台机接受的请求数,或每台机的响应时间等,来调整我们的负载均衡算法。 + +>- 整理:沉默王二,原文链接:https://zhuanlan.zhihu.com/p/73628158 +>- https://segmentfault.com/a/1190000038256027 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/how-jvm-run-zijiema-zhiling.md b/docs/jvm/how-jvm-run-zijiema-zhiling.md new file mode 100644 index 0000000000..91c7e6b973 --- /dev/null +++ b/docs/jvm/how-jvm-run-zijiema-zhiling.md @@ -0,0 +1,599 @@ +--- +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 虚拟机的执行引擎是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。 + +### 一. 运行时栈帧结构 + +栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。**每一个方法从调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。** + +每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。 + +一个线程中的方法调用链可能会很长,很多方法都处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法成为当前方法。执行引擎运行的所有字节码指令对当前栈帧进行操作,在概念模型上,典型的栈帧结构如下图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-a58ee82e-c0b0-4c06-9606-f7a0f0df0de9) + +#### 局部变量表 + +局部变量表(Local Variables Table)用来保存方法中的局部变量,以及方法参数。当 Java 源代码文件被编译成 class 文件的时候,局部变量表的最大容量就已经确定了。 + +我们来看这样一段代码。 + +```java +public class LocalVaraiablesTable { + private void write(int age) { + String name = "沉默王二"; + } +} +``` + +`write()` 方法有一个参数 age,一个局部变量 name。 + +然后用 Intellij IDEA 的 jclasslib 查看一下编译后的字节码文件 LocalVaraiablesTable.class。可以看到 `write()` 方法的 Code 属性中,Maximum local variables(局部变量表的最大容量)的值为 3。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-70ab6bf6-4fbb-4722-99b4-a93d5061630c.png) + +按理说,局部变量表的最大容量应该为 2 才对,一个 age,一个 name,为什么是 3 呢? + +当一个成员方法(非静态方法)被调用时,第 0 个变量其实是调用这个成员方法的对象引用,也就是那个大名鼎鼎的 this。调用方法 `write(18)`,实际上是调用 `write(this, 18)`。 + +点开 Code 属性,查看 LocalVaraiableTable 就可以看到详细的信息了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-e5e6037c-9be1-472f-8ab3-2754466e7828.png) + +第 0 个是 this,类型为 LocalVaraiablesTable 对象;第 1 个是方法参数 age,类型为整型 int;第 2 个是方法内部的局部变量 name,类型为字符串 String。 + +当然了,局部变量表的大小并不是方法中所有局部变量的数量之和,它与变量的类型和变量的作用域有关。当一个局部变量的作用域结束了,它占用的局部变量表中的位置就被接下来的局部变量取代了。 + +来看下面这段代码。 + +```java +public static void method() { + // ① + if (true) { + // ② + String name = "沉默王二"; + } + // ③ + if(true) { + // ④ + int age = 18; + } + // ⑤ +} +``` + +- `method()` 方法的局部变量表大小为 1,因为是静态方法,所以不需要添加 this 作为局部变量表的第一个元素; +- ②的时候局部变量有一个 name,局部变量表的大小变为 1; +- ③的时候 name 变量的作用域结束; +- ④的时候局部变量有一个 age,局部变量表的大小为 1; +- ⑤的时候局 age 变量的作用域结束; + +关于局部变量的作用域,《Effective Java》 中的第 57 条建议: + +>将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。 + +在此,我还有一点要提醒大家。为了尽可能节省栈帧耗用的内存空间,局部变量表中的槽是可以重用的,就像 `method()` 方法演示的那样,这就意味着,合理的作用域有助于提高程序的性能。 + +局部变量表的容量以槽(slot)为最小单位,一个槽可以容纳一个 32 位的数据类型(比如说 int,当然了,《Java 虚拟机规范》中没有明确指出一个槽应该占用的内存空间大小,但我认为这样更容易理解),像 float 和 double 这种明确占用 64 位的数据类型会占用两个紧挨着的槽。 + +来看下面的代码。 + +```java +public void solt() { + double d = 1.0; + int i = 1; +} +``` + +用 jclasslib 可以查看到,`solt()` 方法的 Maximum local variables 的值为 4。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-6734774b-376c-49bf-a915-508c7e829557.png) + +为什么等于 4 呢?带上 this 也就 3 个呀? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-91ad04f8-1620-44c9-83d1-6fbd7860701a.png) + +查看 LocalVaraiableTable 就明白了,变量 i 的下标为 3,也就意味着变量 d 占了两个槽。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-630b50e3-fc37-4748-8d20-852d5358f87a.png) + +#### 操作数栈 + +同局部变量表一样,操作数栈(Operand Stack)的最大深度也在编译的时候就确定了,被写入到了 Code 属性的 maximum stack size 中。当一个方法刚开始执行的时候,操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和取出数据,也就是入栈和出栈操作。 + +来看下面这段代码。 + +```java +public class OperandStack { + public void test() { + add(1,2); + } + + private int add(int a, int b) { + return a + b; + } +} +``` + +OperandStack 类共有 2 个方法,`test()` 方法中调用了 `add()` 方法,传递了 2 个参数。用 jclasslib 可以看到,`test()` 方法的 maximum stack size 的值为 3。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-f790aa0f-d742-465b-91bf-5f143ee098c1.png) + +这是因为调用成员方法的时候会将 this 和所有参数压入栈中,调用完毕后 this 和参数都会一一出栈。通过 「Bytecode」 面板可以查看到对应的字节码指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-c37add5c-a74b-4bd6-8c8e-9085d9e6d374.png) + +- aload_0 用于将局部变量表中下标为 0 的引用类型的变量,也就是 this 加载到操作数栈中; +- iconst_1 用于将整数 1 加载到操作数栈中; +- iconst_2 用于将整数 2 加载到操作数栈中; +- invokevirtual 用于调用对象的成员方法; +- pop 用于将栈顶的值出栈; +- return 为 void 方法的返回指令。 + +再来看一下 `add()` 方法的字节码指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-49e3f396-7ea8-49f5-81d4-093b9bdfa453.png) + +- iload_1 用于将局部变量表中下标为 1 的 int 类型变量加载到操作数栈上(下标为 0 的是 this); +- iload_2 用于将局部变量表中下标为 2 的 int 类型变量加载到操作数栈上; +- iadd 用于 int 类型的加法运算; +- ireturn 为返回值为 int 的方法返回指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-3ffdbe03-c0e4-49de-97a6-76666964a087.png) + +操作数中的数据类型必须与字节码指令匹配,以上面的 iadd 指令为例,该指令只能用于整型数据的加法运算,它在执行的时候,栈顶的两个数据必须是 int 类型的,不能出现一个 long 型和一个 double 型的数据进行 iadd 命令相加的情况。 + +#### 动态链接 + +每个栈帧都包含了一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。 + +来看下面这段代码。 + +```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(); + } +} +``` + +大家对 Java 重写有了解的话,应该能看懂这段代码的意思。Man 类和 Woman 类继承了 Human 类,并且重写了 `sayHello()` 方法。来看一下运行结果: + +``` +男人哭吧哭吧不是罪 +山下的女人是老虎 +山下的女人是老虎 +``` + +这个运行结果很好理解,man 的引用类型为 Human,但指向的是 Man 对象,woman 的引用类型也为 Human,但指向的是 Woman 对象;之后,man 又指向了新的 Woman 对象。 + +从面向对象编程的角度,从多态的角度,我们对运行结果是很好理解的,但站在 Java 虚拟机的角度,它是如何判断 man 和 woman 该调用哪个方法的呢? + +用 jclasslib 看一下 main 方法的字节码指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-93a21aaf-ff67-445d-8ddb-ac6f72fd9b25.png) + +- 第 1 行:new 指令创建了一个 Man 对象,并将对象的内存地址压入栈中。 +- 第 2 行:dup 指令将栈顶的值复制一份并压入栈顶。因为接下来的指令 invokespecial 会消耗掉一个当前类的引用,所以需要复制一份。 +- 第 3 行:invokespecial 指令用于调用构造方法进行初始化。 +- 第 4 行:astore_1,Java 虚拟机从栈顶弹出 Man 对象的引用,然后将其存入下标为 1 局部变量 man 中。 +- 第 5、6、7、8 行的指令和第 1、2、3、4 行类似,不同的是 Woman 对象。 +- 第 9 行:aload_1 指令将第局部变量 man 压入操作数栈中。 +- 第 10 行:invokevirtual 指令调用对象的成员方法 `sayHello()`,注意此时的对象类型为 `com/itwanger/jvm/DynamicLinking$Human`。 +- 第 11 行:aload_2 指令将第局部变量 woman 压入操作数栈中。 +- 第 12 行同第 10 行。 + +注意,从字节码的角度来看,`man.sayHello()`(第 10 行)和 `woman.sayHello()`(第 12 行)的字节码是完全相同的,但我们都知道,这两句指令最终执行的目标方法并不相同。 + +究竟发生了什么呢? + +还得从 `invokevirtual` 这个指令着手,看它是如何实现多态的。根据《Java 虚拟机规范》,invokevirtual 指令在运行时的解析过程可以分为以下几步: + +>①、找到操作数栈顶的元素所指向的对象的实际类型,记作 C。 +②、如果在类型 C 中找到与常量池中的描述符匹配的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;否则返回 `java.lang.IllegalAccessError` 异常。 +③、否则,按照继承关系从下往上一次对 C 的各个父类进行第二步的搜索和验证。 +④、如果始终没有找到合适的方法,则抛出 `java.lang.AbstractMethodError` 异常。 + +也就是说,invokevirtual 指令在第一步的时候就确定了运行时的实际类型,所以两次调用中的 invokevirtual 指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接受者的实际类型来选择方法版本,这个过程就是 Java 重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的过程称为**动态链接**。 + +#### 方法返回地址 + +当一个方法开始执行后,只有两种方式可以退出这个方法: + +- 正常退出,可能会有返回值传递给上层的方法调用者,方法是否有返回值以及返回值的类型根据方法返回的指令来决定,像之前提到的 ireturn 用于返回 int 类型,return 用于 void 方法;还有其他的一些,lreturn 用于 long 型,freturn 用于 float,dreturn 用于 double,areturn 用于引用类型。 + +- 异常退出,方法在执行的过程中遇到了异常,并且没有得到妥善的处理,这种情况下,是不会给它的上层调用者返回任何值的。 + +无论是哪种方式退出,在方法退出后,都必须返回到方法最初被调用时的位置,程序才能继续执行。一般来说,方法正常退出的时候,PC 计数器的值会作为返回地址,栈帧中很可能会保存这个计数器的值,异常退出时则不会。 + +方法退出的过程实际上等同于把当前栈帧出栈,因此接下来可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整 PC 计数器的值,找到下一条要执行的指令等。 + + +#### 附加信息 + +虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,成为栈帧信息。 + +### 二. 方法调用 + +方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。 + +在程序运行时,进行方法调用是最为普遍、频繁的操作。前面说过 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 实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。 + +无论是解释执行还是编译执行,无论是物理机还是虚拟机,对于应用程序,机器都不可能像人一样阅读、理解,然后获得执行能力。大部分的程序代码到物理机的目标代码或者虚拟机执行的指令之前,都需要经过下图中的各个步骤。下图中最下面的那条分支,就是传统编译原理中程序代码到目标机器代码的生成过程;中间那条分支,则是解释执行的过程。 + +![](http://cdn.tobebetterjavaer.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 + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/how-run-java-code.md b/docs/jvm/how-run-java-code.md new file mode 100644 index 0000000000..6559bc52e4 --- /dev/null +++ b/docs/jvm/how-run-java-code.md @@ -0,0 +1,184 @@ +--- +title: JVM到底是如何运行Java代码的? +shortTitle: JVM是如何运行Java代码的? +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,JVM到底是如何运行Java代码的? +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机 +--- + +# JVM到底是如何运行Java代码的? + + +“二哥,看了上一篇 [Hello World](https://mp.weixin.qq.com/s/191I_2CVOxVuyfLVtb4jhg) 的程序后,我很好奇,它是怎么在 Run 面板里打印出‘三妹,少看手机少打游戏,好好学,美美哒’呢?”三妹咪了一口麦香可可奶茶后对我说。 + +“三妹,我们通常把 Java 分为编译期和运行时,弄清楚这两个阶段就知道原因了。” + +贴一下 HelloWorld 这段代码: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); + } +} +``` + +点击 IDEA 工具栏中的锤子按钮(Build Project,编译整个项目),如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-01.png) + + +这时候,就可以在 src 的同级目录 target 下找到一个名为 HelloWorld.class 的文件。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-02.png) + + +如果找不到的话,在目录上右键选择「Reload from Disk,从磁盘上重新加载」,如下图所示: + +![](http://cdn.tobebetterjavaer.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 反编译工具将字节码文件(后缀为 .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」查看,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-04.png) + +PS:字节码并不是机器码,操作系统无法直接识别,需要在操作系统上安装不同版本的 Java 虚拟机(JVM)来识别。通常情况下,我们只需要安装不同版本的 JDK(Java Development Kit,Java 开发工具包)就行了,它里面包含了 JRE(Java Runtime Environment,Java 运行时环境),而 JRE 又包含了 JVM。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-05.png) + +Windows、Linux、MacOS 等操作系统都有相应的 JDK,只要安装好了 JDK 就有了 Java 语言的运行时环境,就可以把一份字节码文件在不同的平台上运行了。可以在 [Oracle 官网](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html)上下载不同版本的 JDK。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-06.png) + +PPS:为什么要查看字节码呢?查看字节码文件更容易让我们搞懂 Java 代码背后的原理,比如搞懂 Java 中的各种语法糖的本质。 + +相比于 IDEA 自带的「Show Bytecode」功能,我更推荐 `jclasslib` 这款插件,可以在插件市场中安装(我已经安装过了)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-07.png) + +安装完成之后,点击 View -> Show Bytecode With jclasslib 即可通过 jclasslib 查看字节码文件了(点击之前,光标要停留在对应的类文件上),如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-08.png) + +使用 jclasslib 不仅可以直观地查看类对应的字节码文件,还可以查看类的基本信息、常量池、接口、字段、方法等信息,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-09.png) + + + +也就是说,在编译阶段,Java 会将 Java 源代码文件编译为字节码文件。在这个阶段,编译器会进行一些检查工作,比如说,某个关键字是不是写错了,语法上是不是符合预期了,不能有很明显的错误,否则带到运行时再检查出来就会比较麻烦了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-10.png) + +Java 字节码是沟通 JVM 与 Java 代码的桥梁,下面使用 javap 来稍微看一下字节码到底长什么样子。 + +``` +0 getstatic #2 +3 ldc #3 +5 invokevirtual #4 +8 return +``` + +Java 虚拟机采用基于栈的架构,其指令由操作码和操作数组成。这些[字节码指令](https://tobebetterjavaer.com/jvm/zijiema-zhiling.html),就叫作 opcode。其中,getstatic、ldc、invokevirtual、return 等,就是 opcode,可以看到是比较容易理解的。 + +我们继续使用 hexdump 看一下字节码的二进制内容。与以上字节码对应的二进制,就是下面这几个数字: + +``` +b2 00 02 12 03 b6 00 04 b1 +``` + +> 注意:这里是二进制文件的16进制表示,也就是hex,一般分析二进制文件都是以hex进行分析。 + +我们可以看一下它们的对应关系。 + +``` +0xb2 getstatic 获取静态字段的值 +0x12 ldc 常量池中的常量值入栈 +0xb6 invokevirtual 运行时方法绑定调用方法 +0xb1 return void 函数返回 +``` + +opcode 有一个字节的长度(0~255),意味着指令集的操作码个数不能操作 256 条。而紧跟在 opcode 后面的是被操作数。比如 b2 00 02,就代表了 `getstatic #2 `。 + +JVM 就是靠解析这些 opcode 和操作数来完成程序的执行的。当我们使用 Java 命令运行 .class 文件的时候,实际上就相当于启动了一个 JVM 进程。 + +然后 JVM 会翻译这些字节码,它有两种执行方式。常见的就是解释执行,将 opcode + 操作数翻译成机器代码;另外一种执行方式就是 [JIT](https://tobebetterjavaer.com/jvm/jit.html),也就是我们常说的即时编译,它会在一定条件下将字节码编译成机器码之后再执行。 + +这些 .class 文件会被加载、存放到 metaspace 中,等待被调用,这里会有一个[类加载器](https://tobebetterjavaer.com/jvm/class-load.html)的概念。 + +而 JVM 的程序运行,都是在栈上完成的,这和其他普通程序的执行是类似的,同样分为堆和栈。比如我们现在运行到了 main 方法,就会给它分配一个栈帧。当退出方法体时,会弹出相应的栈帧。你会发现,大多数字节码指令,就是不断的对栈帧进行操作。 + +而其他大块数据,是存放在堆上的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-run-java-code-91dac706-1c4e-4775-bc4e-b2104283aa04.png) + + diff --git a/docs/src/jvm/hsdb.md b/docs/jvm/hsdb.md similarity index 76% rename from docs/src/jvm/hsdb.md rename to docs/jvm/hsdb.md index 06eccf8cec..e48323a242 100644 --- a/docs/src/jvm/hsdb.md +++ b/docs/jvm/hsdb.md @@ -1,17 +1,18 @@ --- -title: 如何调试 JVM 运行时数据?HSDB(Hotspot Debugger)从入门到实战 -shortTitle: 如何调试 JVM 运行时数据? +title: HSDB(Hotspot Debugger)从入门到实战 +shortTitle: HSDB从入门到实战 category: - Java核心 tag: - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,HSDB(Hotspot Debugger)从入门到实战 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,HSDB(Hotspot Debugger)从入门到实战 head: - - meta - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,HSDB + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,HSDB --- +# HSDB(Hotspot Debugger)从入门到实战 `HSDB(Hotspot Debugger)`,是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除。 @@ -77,33 +78,33 @@ $ jps ### 可视化线程栈 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-a606c6ac-1cfc-44c3-8fdf-e0eeeabbf05a.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-7e4ebd1f-ba9c-4862-b4c3-574de5c30d6b.png) ### 元数据区 @@ -113,18 +114,18 @@ HotSpot VM 里有一套对象专门用来存放元数据,它们包括:  * `ConstantPool/ConstantPoolCache` 对象:每个 `InstanceKlass` 关联着一个 `ConstantPool`,作为该类型的运行时常量池。这个常量池的结构跟 Class 文件里的常量池基本上是对应的 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-07d80d18-be4e-4861-bea3-291eea0ff262.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-85c6fca7-2d1f-4194-bc07-fd1e2ab18632.png) * `Symbol` 对象,对应 Class 文件常量池里的 `JVM_CONSTANT_Utf8` 类型的常量。有一个 VM 全局的 `SymbolTable` 管理着所有 `Symbol`。`Symbol` 由所有 Java 类所共享。 @@ -164,23 +165,23 @@ class VMShow { 首先查看对象直方图可以找到三个 VMShow 对象 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-e5f0f200-83fd-4529-b5d6-416f9a6f626b.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-51fb09f4-06d7-4518-8038-5dd69d765862.png) ### 分析字符串字面量存储区域 @@ -203,7 +204,7 @@ public class StringTest { 2. 打开对象直方图发现只有 1 个 `a` 的字符串对象 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-b5631e70-be3a-48de-99be-4468632d23e0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-b5631e70-be3a-48de-99be-4468632d23e0.png) 3. 查找 StringTable 中 `a` 的对象地址 @@ -213,7 +214,7 @@ jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUti 可以根据需要改变 `matches` 中的值来匹配 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-76084b8d-6134-4866-8891-c24a43a3b836.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-76084b8d-6134-4866-8891-c24a43a3b836.png) 可以看到这个对象地址就是 StringTable 中引用的地址 @@ -221,7 +222,7 @@ jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUti 5. 重新使用对象直方图查看 String 值 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-f3c35027-2fe1-4b18-8a1c-b7243a9b5149.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-f3c35027-2fe1-4b18-8a1c-b7243a9b5149.png) 这里有5个值,`ab` 有3个: @@ -236,13 +237,13 @@ jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUti ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-f07a9fc8-5744-4859-9df9-c3a1016e936a.png) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-88e8c10d-e09c-4a74-95e6-e5b21577459f.png) ### 分析String.intern @@ -333,30 +334,30 @@ public class StringInternTest { 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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.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) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-47a42bfa-7645-4c9b-bcd8-aeabea1ae44f.png) +--- -参考链接:https://juejin.cn/post/7072344870374866951 +>参考链接:https://zzcoder.cn/2019/12/06/HSDB%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E5%AE%9E%E6%88%98/ ---- -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/) - +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/jit.md b/docs/jvm/jit.md new file mode 100644 index 0000000000..6ec401358a --- /dev/null +++ b/docs/jvm/jit.md @@ -0,0 +1,768 @@ +--- +title: Java即时编译(JIT)器原理解析及实践 +shortTitle: JIT原理解析及实践 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java即时编译(JIT)器原理解析及实践 +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,jit +--- + +# Java即时编译(JIT)器原理解析及实践 + + +## 一、导读 + +常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行。而Java为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由javac编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java通常不如C++这类编译型语言。 + +为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。 + +即时编译器极大地提高了Java程序的运行速度,而且跟静态编译相比,即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。目前,即时编译器已经非常成熟了,在性能层面甚至可以和编译型语言相比。不过在这个领域,大家依然在不断探索如何结合不同的编译方式,使用更加智能的手段来提升程序的运行速度。 + +## 二、Java的执行过程 + +Java的执行过程整体可以分为两个部分,第一步由javac将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析,编译原理中这部分的编译称为前端编译。接下来无需编译直接逐条将字节码解释执行,在解释执行的过程中,虚拟机同时对程序运行的信息进行收集,在这些信息的基础上,编译器会逐渐发挥作用,它会进行后端编译——把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译。 + +怎么样才会被认为是热点代码呢?JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能。整体的执行过程大致如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-9a62fc02-1a6a-451e-bb2b-19fc086d5be0.png) + +### 1\. JVM中的编译器 + +JVM中集成了两种编译器,Client Compiler和Server Compiler,它们的作用也不同。Client Compiler注重启动速度和局部的优化,Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢。两种编译器有着不同的应用场景,在虚拟机中同时发挥作用。 + +**Client Compiler** + +HotSpot VM带有一个Client Compiler C1编译器。这种编译器启动速度快,但是性能比较Server Compiler来说会差一些。C1会做三件事: + +* 局部简单可靠的优化,比如字节码上进行的一些基础优化,方法内联、常量传播等,放弃许多耗时较长的全局优化。 +* 将字节码构造成高级中间表示(High-level Intermediate Representation,以下称为HIR),HIR与平台无关,通常采用图结构,更适合JVM对程序进行优化。 +* 最后将HIR转换成低级中间表示(Low-level Intermediate Representation,以下称为LIR),在LIR的基础上会进行寄存器分配、窥孔优化(局部的优化方式,编译器在一个基本块或者多个基本块中,针对已经生成的代码,结合CPU自己指令的特点,通过一些认为可能带来性能提升的转换规则或者通过整体的分析,进行指令转换,来提升代码性能)等操作,最终生成机器码。 + +**Server Compiler** + +Server Compiler主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比Client Compiler高30%以上。目前,Hotspot虚拟机中使用的Server Compiler有两种:C2和Graal。 + +**C2 Compiler** + +在Hotspot VM中,默认的Server Compiler是C2编译器。 + +C2编译器在进行编译优化时,会使用一种控制流与数据流结合的图数据结构,称为Ideal Graph。 Ideal Graph表示当前程序的数据流向和指令间的依赖关系,依靠这种图结构,某些优化步骤(尤其是涉及浮动代码块的那些优化步骤)变得不那么复杂。 + +Ideal Graph的构建是在解析字节码的时候,根据字节码中的指令向一个空的Graph中添加节点,Graph中的节点通常对应一个指令块,每个指令块包含多条相关联的指令,JVM会利用一些优化技术对这些指令进行优化,比如Global Value Numbering、常量折叠等,解析结束后,还会进行一些死代码剔除的操作。生成Ideal Graph后,会在这个基础上结合收集的程序运行信息来进行一些全局的优化,这个阶段如果JVM判断此时没有全局优化的必要,就会跳过这部分优化。 + +无论是否进行全局优化,Ideal Graph都会被转化为一种更接近机器层面的MachNode Graph,最后编译的机器码就是从MachNode Graph中得的,生成机器码前还会有一些包括寄存器分配、窥孔优化等操作。关于Ideal Graph和各种全局的优化手段会在后面的章节详细介绍。Server Compiler编译优化的过程如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-f4d1b763-be02-4bb2-ab0e-45b1f0eb9550.png) + +**Graal Compiler** + +从JDK 9开始,Hotspot VM中集成了一种新的Server Compiler,Graal编译器。相比C2编译器,Graal有这样几种关键特性: + +* 前文有提到,JVM会在解释执行的时候收集程序运行的各种信息,然后编译器会根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal比C2更加青睐这种优化,所以Graal的峰值性能通常要比C2更好。 +* 使用Java编写,对于Java语言,尤其是新特性,比如Lambda、Stream等更加友好。 +* 更深层次的优化,比如虚函数的内联、部分逃逸分析等。 + +Graal编译器可以通过Java虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler启用。当启用时,它将替换掉HotSpot中的C2编译器,并响应原本由C2负责的编译请求。 + +### 2\. 分层编译 + +在Java 7以前,需要研发人员根据服务的性质去选择编译器。对于需要快速启动的,或者一些不会长期运行的服务,可以采用编译效率较高的C1,对应参数-client。长期运行的服务,或者对峰值性能有要求的后台服务,可以采用峰值性能更好的C2,对应参数-server。Java 7开始引入了分层编译的概念,它结合了C1和C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次。五个层级分别是: + +1. 解释执行。 +2. 执行不带profiling的C1代码。 +3. 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码。 +4. 执行带所有profiling的C1代码。 +5. 执行C2代码。 + +profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。 + +通常情况下,C2代码的执行效率要比C1代码的高出30%以上。C1层执行的代码,按执行效率排序从高至低则是1层>2层>3层。这5个层次中,1层和4层都是终止状态,当一个方法到达终止状态后,只要编译后的代码并没有失效,那么JVM就不会再次发出该方法的编译请求的。服务实际运行时,JVM会根据服务运行情况,从解释执行开始,选择不同的编译路径,直到到达终止状态。下图中就列举了几种常见的编译路径: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-a6cebc82-ed4d-4b6d-892a-c5b245d227ab.png) + +* 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。 +* 如果方法比较小(比如Java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码的执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,直接选择用1层的C1编译运行。 +* 在C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling ,根据信息直接由第4层的C2编译。 +* 前文提到C1中的执行效率是1层>2层>3层,第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。 +* 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代表的就是反优化。 + +总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。 + +### 3\. 即时编译的触发 + +Java虚拟机根据方法的调用次数以及循环回边的执行次数来触发即时编译。循环回边是一个控制流图中的概念,程序中可以简单理解为往回跳转的指令,比如下面这段代码: + +循环回边 + +```java +public void nlp(Object obj) { + int sum = 0; + for (int i = 0; i < 200; i++) { + sum += i; + } +} + +``` + +上面这段代码经过编译生成下面的字节码。其中,偏移量为18的字节码将往回跳至偏移量为4的字节码中。在解释执行时,每当运行一次该指令,Java虚拟机便会将该方法的循环回边计数器加1。 + +字节码 + +``` +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 + +``` + +在即时编译过程中,编译器会识别循环的头部和尾部。上面这段字节码中,循环体的头部和尾部分别为偏移量为11的字节码和偏移量为15的字节码。编译器将在循环体结尾增加循环回边计数器的代码,来对循环进行计数。 + +当方法的调用次数和循环回边的次数的和,超过由参数-XX:CompileThreshold指定的阈值时(使用C1时,默认值为1500;使用C2时,默认值为10000),就会触发即时编译。 + +开启分层编译的情况下,-XX:CompileThreshold参数设置的阈值将会失效,触发编译会由以下的条件来判断: + +* 方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数。 +* 方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时。 + +分层编译触发条件公式 + +``` +i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s) +i为调用次数,b是循环回边次数 + +``` + +上述满足其中一个条件就会触发即时编译,并且JVM会根据当前的编译方法数以及编译线程数动态调整系数s。 + +## 三、编译优化 + +即时编译器会对正在运行的服务进行一系列的优化,包括字节码解析过程中的分析,根据编译过程中代码的一些中间形式来做局部优化,还会根据程序依赖图进行全局优化,最后才会生成机器码。 + +### 1\. 中间表达形式(Intermediate Representation) + +在编译原理中,通常把编译器分为前端和后端,前端编译经过词法分析、语法分析、语义分析生成中间表达形式(Intermediate Representation,以下称为IR),后端会对IR进行优化,生成目标代码。 + +Java字节码就是一种IR,但是字节码的结构复杂,字节码这样代码形式的IR也不适合做全局的分析优化。现代编译器一般采用图结构的IR,静态单赋值(Static Single Assignment,SSA)IR是目前比较常用的一种。这种IR的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用。举个例子: + +SSA IR + +``` +Plain Text +{ + a = 1; + a = 2; + b = a; +} + +``` + +上述代码中我们可以轻易地发现a = 1的赋值是冗余的,但是编译器不能。传统的编译器需要借助数据流分析,从后至前依次确认哪些变量的值被覆盖掉。不过,如果借助了SSA IR,编译器则可以很容易识别冗余赋值。 + +上面代码的SSA IR形式的伪代码可以表示为: + +SSA IR + +``` +Plain Text +{ + 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)的例子: + +DeadCodeElimination + +```java +public void DeadCodeElimination{ + int a = 2; + int b = 0 + if(2 > 1){ + a = 1; + } else{ + b = 2; + } + add(a,b) +} + +``` + +可以得到SSA IR伪代码: + +DeadCodeElimination + +``` +a_1 = 2; +b_1 = 0 +if true: + a_2 = 1; +else + b_2 = 2; +add(a,b) + +``` + +编译器通过执行字节码可以发现 b_2 赋值后不会被使用,else分支不会被执行。经过死代码删除后就可以得到代码: + +DeadCodeElimination + +```java +public void DeadCodeElimination{ + int a = 1; + int b = 0; + add(a,b) +} + +``` + +我们可以将编译器的每一种优化看成一个图优化算法,它接收一个IR图,并输出经过转换后的IR图。编译器优化的过程就是一个个图节点的优化串联起来的。 + +**C1中的中间表达形式** + +前文提及C1编译器内部使用高级中间表达形式HIR,低级中间表达形式LIR来进行各种优化,这两种IR都是SSA形式的。 + +HIR是由很多基本块(Basic Block)组成的控制流图结构,每个块包含很多SSA形式的指令。基本块的结构如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-037b406d-1040-4bf8-976c-abf14a92402d.png) + +其中,predecessors表示前驱基本块(由于前驱可能是多个,所以是BlockList结构,是多个BlockBegin组成的可扩容数组)。同样,successors表示多个后继基本块BlockEnd。除了这两部分就是主体块,里面包含程序执行的指令和一个next指针,指向下一个执行的主体块。 + +从字节码到HIR的构造最终调用的是GraphBuilder,GraphBuilder会遍历字节码构造所有代码基本块储存为一个链表结构,但是这个时候的基本块只有BlockBegin,不包括具体的指令。第二步GraphBuilder会用一个ValueStack作为操作数栈和局部变量表,模拟执行字节码,构造出对应的HIR,填充之前空的基本块,这里给出简单字节码块构造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,形式上更加简化。 + +**Sea-of-Nodes IR** + +C2编译器中的Ideal Graph采用的是一种名为Sea-of-Nodes中间表达形式,同样也是SSA形式的。它最大特点是去除了变量的概念,直接采用值来进行运算。为了方便理解,可以利用IR可视化工具Ideal Graph Visualizer(IGV),来展示具体的IR图。比如下面这段代码: + +example + +```java +public static int foo(int count) { + int sum = 0; + for (int i = 0; i < count; i++) { + sum += i; + } + return sum; +} + +``` + +对应的IR图如下所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-f96da42a-568b-45ba-bed1-f4238ac32e14.png) + +图中若干个顺序执行的节点将被包含在同一个基本块之中,如图中的B0、B1等。B0基本块中0号Start节点是方法入口,B3中21号Return节点是方法出口。红色加粗线条为控制流,蓝色线条为数据流,而其他颜色的线条则是特殊的控制流或数据流。被控制流边所连接的是固定节点,其他的则是浮动节点(浮动节点指只要能满足数据依赖关系,可以放在不同位置的节点,浮动节点变动的这个过程称为Schedule)。 + +这种图具有轻量级的边结构。 图中的边仅由指向另一个节点的指针表示。节点是Node子类的实例,带有指定输入边的指针数组。这种表示的优点是改变节点的输入边很快,如果想要改变输入边,只要将指针指向Node,然后存入Node的指针数组就可以了。 + +依赖于这种图结构,通过收集程序运行的信息,JVM可以通过Schedule那些浮动节点,从而获得最好的编译效果。 + +**Phi And Region Nodes** + +Ideal Graph是SSA IR。 由于没有变量的概念,这会带来一个问题,就是不同执行路径可能会对同一变量设置不同的值。例如下面这段代码if语句的两个分支中,分别返回5和6。此时,根据不同的执行路径,所读取到的值很有可能不同。 + +example + +```java +int test(int x) { +int a = 0; + if(x == 1) { + a = 5; + } else { + a = 6; + } + return a; +} + +``` + +为了解决这个问题,就引入一个Phi Nodes的概念,能够根据不同的执行路径选择不同的值。于是,上面这段代码可以表示为下面这张图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-fb8b2bac-a7b9-45eb-bd28-05e35cf043ae.png) + +Phi Nodes中保存不同路径上包含的所有值,Region Nodes根据不同路径的判断条件,从Phi Nodes取得当前执行路径中变量应该赋予的值,带有Phi节点的SSA形式的伪代码如下: + +Phi Nodes + +``` +int test(int x) { + a_1 = 0; + if(x == 1){ + a_2 = 5; + }else { + a_3 = 6; + } + a_4 = Phi(a_2,a_3); + return a_4; +} + +``` + +**Global Value Numbering** + +Global Value Numbering(GVN) 是一种因为Sea-of-Nodes变得非常容易的优化技术 。 + +GVN是指为每一个计算得到的值分配一个独一无二的编号,然后遍历指令寻找优化的机会,它可以发现并消除等价计算的优化技术。如果一段程序中出现了多次操作数相同的乘法,那么即时编译器可以将这些乘法合并为一个,从而降低输出机器码的大小。如果这些乘法出现在同一执行路径上,那么GVN还将省下冗余的乘法操作。在Sea-of-Nodes中,由于只存在值的概念,因此GVN算法将非常简单:即时编译器只需判断该浮动节点是否与已存在的浮动节点的编号相同,所输入的IR节点是否一致,便可以将这两个浮动节点归并成一个。比如下面这段代码: + +GVN + +``` +a = 1; +b = 2; +c = a + b; +d = a + b; +e = d; + +``` + +GVN会利用Hash算法编号,计算a = 1时,得到编号1,计算b = 2时得到编号2,计算c = a + b时得到编号3,这些编号都会放入Hash表中保存,在计算d = a + b时,会发现a + b已经存在Hash表中,就不会再进行计算,直接从Hash表中取出计算过的值。最后的e = d也可以由Hash表中查到而进行复用。 + +可以将GVN理解为在IR图上的公共子表达式消除(Common Subexpression Elimination,CSE)。两者区别在于,GVN直接比较值的相同与否,而CSE是借助词法分析器来判断两个表达式相同与否。 + +### 2.方法内联 + +方法内联,是指在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。JIT大部分的优化都是在内联的基础上进行的,方法内联是即时编译器中非常重要的一环。 + +Java服务中存在大量getter/setter方法,如果没有方法内联,在调用getter/setter时,程序执行时需要保存当前方法的执行位置,创建并压入用于getter/setter的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。内联了对 getter/setter的方法调用后,上述操作仅剩字段访问。在C2编译器 中,方法内联在解析字节码的过程中完成。当遇到方法调用字节码时,编译器将根据一些阈值参数决定是否需要内联当前方法的调用。如果需要内联,则开始解析目标方法的字节码。比如下面这个示例(来源于网络): + +方法内联的过程 + +```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图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-04ca4a7e-46e7-4782-bb43-333aea31ed57.png) + +内联后的IR图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-4bf4d190-7fd2-4542-b948-0c85ee6963d2.png) + +内联不仅将被调用方法的IR图节点复制到调用者方法的IR图中,还要完成其他操作。 + +被调用方法的参数替换为调用者方法进行方法调用时所传入参数。上面例子中,将bar方法中的1号P(0)节点替换为foo方法3号LoadField节点。 + +调用者方法的IR图中,方法调用节点的数据依赖会变成被调用方法的返回。如果存在多个返回节点,会生成一个Phi节点,将这些返回值聚合起来,并作为原方法调用节点的替换对象。图中就是将8号==节点,以及12号Return节点连接到原5号Invoke节点的边,然后指向新生成的24号Phi节点中。 + +如果被调用方法将抛出某种类型的异常,而调用者方法恰好有该异常类型的处理器,并且该异常处理器覆盖这一方法调用,那么即时编译器需要将被调用方法抛出异常的路径,与调用者方法的异常处理器相连接。 + +**方法内联的条件** + +编译器的大部分优化都是在方法内联的基础上。所以一般来说,内联的方法越多,生成代码的执行效率越高。但是对于即时编译器来说,内联的方法越多,编译时间也就越长,程序达到峰值性能的时刻也就比较晚。 + +可以通过虚拟机参数-XX:MaxInlineLevel调整内联的层数,以及1层的直接递归调用(可以通过虚拟机参数-XX:MaxRecursiveInlineLevel调整)。一些常见的内联相关的参数如下表所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-48e4ff65-07ec-487e-8b08-2f8fed1e56bd.png) + +**虚函数内联** + +内联是JIT提升性能的主要手段,但是虚函数使得内联是很难的,因为在内联阶段并不知道他们会调用哪个方法。例如,我们有一个数据处理的接口,这个接口中的一个方法有三种实现add、sub和multi,JVM是通过保存虚函数表Virtual Method Table(以下称为VMT)存储class对象中所有的虚函数,class的实例对象保存着一个VMT的指针,程序运行时首先加载实例对象,然后通过实例对象找到VMT,通过VMT找到对应方法的地址,所以虚函数的调用比直接指向方法地址的classic call性能上会差一些。很不幸的是,Java中所有非私有的成员函数的调用都是虚调用。 + +C2编译器已经足够智能,能够检测这种情况并会对虚调用进行优化。比如下面这段代码例子: + +virtual call + +```java +public class SimpleInliningTest +{ + public static void main(String[] args) throws InterruptedException { + VirtualInvokeTest obj = new VirtualInvokeTest(); + VirtualInvoke1 obj1 = new VirtualInvoke1(); + for (int i = 0; i < 100000; i++) { + invokeMethod(obj); + invokeMethod(obj1); + } + Thread.sleep(1000); + } +​ + public static void invokeMethod(VirtualInvokeTest obj) { + obj.methodCall(); + } +​ + private static class VirtualInvokeTest { + public void methodCall() { + System.out.println("virtual call"); + } + } +​ + private static class VirtualInvoke1 extends VirtualInvokeTest { + @Override + public void methodCall() { + super.methodCall(); + } + } +} + +``` + +经过JIT编译器优化后,进行反汇编得到下面这段汇编代码: + +``` + 0x0000000113369d37: callq 0x00000001132950a0 ; OopMap{off=476} + ;*invokevirtual methodCall //代表虚调用 + ; - SimpleInliningTest::invokeMethod@1 (line 18) + ; {optimized virtual_call} //虚调用已经被优化 + +``` + +可以看到JIT对methodCall方法进行了虚调用优化optimized virtual_call。经过优化后的方法可以被内联。但是C2编译器的能力有限,对于多个实现方法的虚调用就“无能为力”了。 + +比如下面这段代码,我们增加一个实现: + +多实现的虚调用 + +```java +public class SimpleInliningTest +{ + public static void main(String[] args) throws InterruptedException { + VirtualInvokeTest obj = new VirtualInvokeTest(); + VirtualInvoke1 obj1 = new VirtualInvoke1(); + VirtualInvoke2 obj2 = new VirtualInvoke2(); + for (int i = 0; i < 100000; i++) { + invokeMethod(obj); + invokeMethod(obj1); + invokeMethod(obj2); + } + Thread.sleep(1000); + } +​ + public static void invokeMethod(VirtualInvokeTest obj) { + obj.methodCall(); + } +​ + private static class VirtualInvokeTest { + public void methodCall() { + System.out.println("virtual call"); + } + } +​ + private static class VirtualInvoke1 extends VirtualInvokeTest { + @Override + public void methodCall() { + super.methodCall(); + } + } + private static class VirtualInvoke2 extends VirtualInvokeTest { + @Override + public void methodCall() { + super.methodCall(); + } + } +} + +``` + +经过反编译得到下面的汇编代码: + +代码块 + +``` + 0x000000011f5f0a37: callq 0x000000011f4fd2e0 ; OopMap{off=28} + ;*invokevirtual methodCall //代表虚调用 + ; - SimpleInliningTest::invokeMethod@1 (line 20) + ; {virtual_call} //虚调用未被优化 + +``` + +可以看到多个实现的虚调用未被优化,依然是virtual_call。 + +Graal编译器针对这种情况,会去收集这部分执行的信息,比如在一段时间,发现前面的接口方法的调用add和sub是各占50%的几率,那么JVM就会在每次运行时,遇到add就把add内联进来,遇到sub的情况再把sub函数内联进来,这样这两个路径的执行效率就会提升。在后续如果遇到其他不常见的情况,JVM就会进行去优化的操作,在那个位置做标记,再遇到这种情况时切换回解释执行。 + +### 3\. 逃逸分析 + +逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”。Java虚拟机的即时编译器会对新建的对象进行逃逸分析,判断对象是否逃逸出线程或者方法。即时编译器判断对象是否逃逸的依据有两种: + +1. 对象是否被存入堆中(静态字段或者堆中对象的实例字段),一旦对象被存入堆中,其他线程便能获得该对象的引用,即时编译器就无法追踪所有使用该对象的代码位置。 +2. 对象是否被传入未知代码中,即时编译器会将未被内联的代码当成未知代码,因为它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中,这种情况,可以直接认为方法调用的调用者以及参数是逃逸的。 + +逃逸分析通常是在方法内联的基础上进行的,即时编译器可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。下面这段代码的就是对象未逃逸的例子: + +```java +pulbic 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; + } + } +} + +``` + +在这个例子中,创建了两个对象foo和bar,其中一个作为另一个方法的参数提供。该方法setFoo()存储对收到的Foo对象的引用。如果Bar对象在堆上,则对Foo的引用将逃逸。但是在这种情况下,编译器可以通过逃逸分析确定Bar对象本身不会对逃逸出example()的调用。这意味着对Foo的引用也不能逃逸。因此,编译器可以安全地在栈上分配两个对象。 + +**锁消除** + +在学习Java并发编程时会了解锁消除,而锁消除就是在逃逸分析的基础上进行的。 + +如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没就有意义。因为线程并不能获得该锁对象。在这种情况下,即时编译器会消除对该不逃逸锁对象的加锁、解锁操作。实际上,编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。由于Java虚拟机即时编译的限制,上述条件被强化为证明锁对象不逃逸出当前编译的方法。不过,基于逃逸分析的锁消除实际上并不多见。 + +**栈上分配** + +我们都知道Java的对象是在堆上分配的,而堆是对所有对象可见的。同时,JVM需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存。如果逃逸分析能够证明某些新建的对象不逃逸,那么JVM完全可以将其分配至栈上,并且在new语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象。不过Hotspot虚拟机,并没有进行实际的栈上分配,而是使用了标量替换这一技术。所谓的标量,就是仅能存储一个值的变量,比如Java代码中的基本类型。与之相反,聚合量则可能同时存储多个值,其中一个典型的例子便是Java的对象。编译器会在方法内将未逃逸的聚合量分解成多个标量,以此来减少堆上分配。下面是一个标量替换的例子: + +标量替换 + +```java +public class Example{ + @AllArgsConstructor + class Cat{ + int age; + int weight; + } + public static void example(){ + Cat cat = new Cat(1,10); + addAgeAndWeight(cat.age,Cat.weight); + } +} + +``` + +经过逃逸分析,cat对象未逃逸出example()的调用,因此可以对聚合量cat进行分解,得到两个标量age和weight,进行标量替换后的伪代码: + +```java +public class Example{ + @AllArgsConstructor + class Cat{ + int age; + int weight; + } + public static void example(){ + int age = 1; + int weight = 10; + addAgeAndWeight(age,weight); + } +} + +``` + +**部分逃逸分析** + +部分逃逸分析也是Graal对于概率预测的应用。通常来说,如果发现一个对象逃逸出了方法或者线程,JVM就不会去进行优化,但是Graal编译器依然会去分析当前程序的执行路径,它会在逃逸分析基础上收集、判断哪些路径上对象会逃逸,哪些不会。然后根据这些信息,在不会逃逸的路径上进行锁消除、栈上分配这些优化手段。 + +### 4\. Loop Transformations + +在文章中介绍C2编译器的部分有提及到,C2编译器在构建Ideal Graph后会进行很多的全局优化,其中就包括对循环的转换,最重要的两种转换就是循环展开和循环分离。 + +**循环展开** + +循环展开是一种循环转换技术,它试图以牺牲程序二进制码大小为代价来优化程序的执行速度,是一种用空间换时间的优化手段。 + +循环展开通过减少或消除控制程序循环的指令,来减少计算开销,这种开销包括增加指向数组中下一个索引或者指令的指针算数等。如果编译器可以提前计算这些索引,并且构建到机器代码指令中,那么程序运行时就可以不必进行这种计算。也就是说有些循环可以写成一些重复独立的代码。比如下面这个循环: + +循环展开 + +```java +public void loopRolling(){ + for(int i = 0;i<200;i++){ + delete(i); + } +} + +``` + +上面的代码需要循环删除200次,通过循环展开可以得到下面这段代码: + +循环展开 + +```java +public void loopRolling(){ + for(int i = 0;i<200;i+=5){ + delete(i); + delete(i+1); + delete(i+2); + delete(i+3); + delete(i+4); + } +} + +``` + +这样展开就可以减少循环的次数,每次循环内的计算也可以利用CPU的流水线提升效率。当然这只是一个示例,实际进行展开时,JVM会去评估展开带来的收益,再决定是否进行展开。 + +**循环分离** + +循环分离也是循环转换的一种手段。它把循环中一次或多次的特殊迭代分离出来,在循环外执行。举个例子,下面这段代码: + +循环分离 + +```java +int a = 10; +for(int i = 0;i<10;i++){ + b[i] = x[i] + x[a]; + a = i; +} + +``` + +可以看出这段代码除了第一次循环a = 10以外,其他的情况a都等于i-1。所以可以把特殊情况分离出去,变成下面这段代码: + +循环分离 + +```java +b[0] = x[0] + 10; +for(int i = 1;i<10;i++){ + b[i] = x[i] + x[i-1]; +} + +``` + +这种等效的转换消除了在循环中对a变量的需求,从而减少了开销。 + +### 5\. 窥孔优化与寄存器分配 + +前文提到的窥孔优化是优化的最后一步,这之后就会程序就会转换成机器码,窥孔优化就是将编译器所生成的中间代码(或目标代码)中相邻指令,将其中的某些组合替换为效率更高的指令组,常见的比如强度削减、常数合并等,看下面这个例子就是一个强度削减的例子: + +强度削减 + +``` +y1=x1*3 经过强度削减后得到 y1=(x1<<1)+x1 + +``` + +编译器使用移位和加法削减乘法的强度,使用更高效率的指令组。 + +寄存器分配也是一种编译的优化手段,在C2编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中,CPU访问寄存器的速度比内存快得多,可以提升程序的运行速度。 + +寄存器分配和窥孔优化是程序优化的最后一步。经过寄存器分配和窥孔优化之后,程序就会被转换成机器码保存在codeCache中。 + +## 四、实践 + +即时编译器情况复杂,同时网络上也很少有实战经验,以下是我们团队的一些调整经验。 + +### 1\. 编译相关的重* 要参数 + +* -XX:+TieredCompilation:开启分层编译,JDK8之后默认开启 +* -XX:+CICompilerCount=N:编译线程数,设置数量后,JVM会自动分配线程数,C1:C2 = 1:2 +* -XX:TierXBackEdgeThreshold:OSR编译的阈值 +* -XX:TierXMinInvocationThreshold:开启分层编译后各层调用的阈值 +* -XX:TierXCompileThreshold:开启分层编译后的编译阈值 +* -XX:ReservedCodeCacheSize:codeCache最大大小 +* -XX:InitialCodeCacheSize:codeCache初始大小 + +-XX:TierXMinInvocationThreshold是开启分层编译的情况下,触发编译的阈值参数,当方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数,或者当方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时,便会触发X层即时编译。分层编译开启下会乘以一个系数,系数根据当前编译的方法和编译线程数确定,降低阈值可以提升编译方法数,一些常用但是不能编译的方法可以编译优化提升性能。 + +由于编译情况复杂,JVM也会动态调整相关的阈值来保证JVM的性能,所以不建议手动调整编译相关的参数。除非一些特定的Case,比如codeCache满了停止了编译,可以适当增加codeCache大小,或者一些非常常用的方法,未被内联到,拖累了性能,可以调整内敛层数或者内联方法的大小来解决。 + +### 2\. 通过JITwatch分析编译日志 + +通过增加-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining -XX:+PrintCodeCache -XX:+PrintCodeCacheOnCompilation -XX:+TraceClassLoading -XX:+LogCompilation -XX:LogFile=LogPath参数可以输出编译、内联、codeCache信息到文件。但是打印的编译日志多且复杂很难直接从其中得到信息,可以使用JITwatch的工具来分析编译日志。JITwatch首页的Open Log选中日志文件,点击Start就可以开始分析日志。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-82ee887c-af7d-48d7-88a0-28960e564d4a.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-6158d832-9a0d-4af0-96ff-bf216a9cd5c6.png) + +如上图所示,区域1中是整个项目Java Class包括引入的第三方依赖;区域2是功能区Timeline以图形的形式展示JIT编译的时间轴,Histo是直方图展示一些信息,TopList里面是编译中产生的一些对象和数据的排序,Cache是空闲codeCache空间,NMethod是Native方法,Threads是JIT编译的线程;区域3是JITwatch对日志分析结果的展示,其中Suggestions中会给出一些代码优化的建议,举个例子,如下图中: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-04b2d9ea-7add-4ee5-bf72-61a6bbaa58cf.png) + +我们可以看到在调用ZipInputStream的read方法时,因为该方法没有被标记为热点方法,同时又“太大了”,导致无法被内联到。使用-XX:CompileCommand中inline指令可以强制方法进行内联,不过还是建议谨慎使用,除非确定某个方法内联会带来不少的性能提升,否则不建议使用,并且过多使用对编译线程和codeCache都会带来不小的压力。 + +区域3中的-Allocs和-Locks逃逸分析后JVM对代码做的优化,包括栈上分配、锁消除等。 + +### 3\. 使用Graal编译器 + +由于JVM会去根据当前的编译方法数和编译线程数对编译阈值进行动态的调整,所以实际服务中对这一部分的调整空间是不大的,JVM做的已经足够多了。 + +为了提升性能,在服务中尝试了最新的Graal编译器。只需要使用-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler就可以启动Graal编译器来代替C2编译器,并且响应C2的编译请求,不过要注意的是,Graal编译器与ZGC不兼容,只能与G1搭配使用。 + +前文有提到过,Graal是一个用Java写的即时编译器,它从Java 9开始便被集成自JDK中,作为实验性质的即时编译器。Graal编译器就是脱身于GraalVM,GraalVM是一个高性能的、支持多种编程语言的执行环境。它既可以在传统的 OpenJDK上运行,也可以通过AOT(Ahead-Of-Time)编译成可执行文件单独运行,甚至可以集成至数据库中运行。 + +前文提到过数次,Graal的优化都基于某种假设(Assumption)。当假设出错的情况下,Java虚拟机会借助去优化(Deoptimization)这项机制,从执行即时编译器生成的机器码切换回解释执行,在必要情况下,它甚至会废弃这份机器码,并在重新收集程序profile之后,再进行编译。 + +这些中激进的手段使得Graal的峰值性能要好于C2,而且在Scale、Ruby这种语言Graal表现更加出色,Twitter目前已经在服务中大量的使用Graal来提升性能,企业版的GraalVM使得Twitter服务性能提升了22%。 + +**使用Graal编译器后性能表现** + +在我们的线上服务中,启用Graal编译后,TP9999从60ms -> 50ms ,下降10ms,下降幅度达16.7%。 + +运行过程中的峰值性能会更高。可以看出对于该服务,Graal编译器带来了一定的性能提升。 + +**Graal编译器的问题** + +Graal编译器的优化方式更加激进,因此在启动时会进行更多的编译,Graal编译器本身也需要被即时编译,所以服务刚启动时性能会比较差。 + +考虑的解决办法:JDK 9开始提供工具jaotc,同时GraalVM的Native Image都是可以通过静态编译,极大地提升服务的启动速度的方式,但是GraalVM会使用自己的垃圾回收,这是一种很原始的基于复制算法的垃圾回收,相比G1、ZGC这些优秀的新型垃圾回收器,它的性能并不好。同时GraalVM对Java的一些特性支持也不够,比如基于配置的支持,比如反射就需要把所有需要反射的类配置一个JSON文件,在大量使用反射的服务,这样的配置会是很大的工作量。我们也在做这方面的调研。 + +## 五、总结 + +本文主要介绍了JIT即时编译的原理以及在美团一些实践的经验,还有最前沿的即时编译器的使用效果。作为一项解释型语言中提升性能的技术,JIT已经比较成熟了,在很多语言中都有使用。对于Java服务,JVM本身已经做了足够多,但是我们还应该不断深入了解JIT的优化原理和最新的编译技术,从而弥补JIT的劣势,提升Java服务的性能,不断追求卓越。 + +----- + +原文链接:https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/jvm/meituan-9-gc.md b/docs/jvm/meituan-9-gc.md similarity index 98% rename from docs/src/jvm/meituan-9-gc.md rename to docs/jvm/meituan-9-gc.md index c8423cf8f1..d524ae2383 100644 --- a/docs/src/jvm/meituan-9-gc.md +++ b/docs/jvm/meituan-9-gc.md @@ -5,12 +5,14 @@ category: - Java核心 tag: - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java中9种常见的CMS GC问题分析与解决 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java中9种常见的CMS GC问题分析与解决 head: - - meta - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,cms + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,cms --- + +# Java中9种常见的CMS GC问题分析与解决 **1.1 引言** @@ -1547,7 +1549,7 @@ gperftools 是 Google 开发的一款非常实用的工具集,它的原理是 ![](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)》。 +除了项目本身的原因,还可能有外部依赖导致的泄漏,如 Netty 和 Spring Boot,详细情况可以学习下这两篇文章:《[疑案追踪:Spring Boot内存泄露排查记](http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651750037&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 小结** @@ -1717,9 +1719,8 @@ JNI 产生的 GC 问题较难排查,需要谨慎使用。 ---- -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/) +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/jvm/neicun-jiegou.md b/docs/jvm/neicun-jiegou.md new file mode 100644 index 0000000000..f25331e185 --- /dev/null +++ b/docs/jvm/neicun-jiegou.md @@ -0,0 +1,119 @@ +--- +title: 深入理解JVM的内存结构 +shortTitle: 深入理解JVM的内存结构 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,深入理解JVM的内存结构 +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,内存结构 +--- + +# 深入理解JVM的内存结构 + + +在谈 JVM 内存区域划分之前,我们先来看一下 Java 程序的具体执行过程,我画了一幅图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/neicun-jiegou-dac0f4c1-8a7e-4309-a599-5664cdaf5016.png) + +Java 源代码文件经过编译器编译后生成字节码文件,然后交给 JVM 的类加载器,加载完毕后,交给执行引擎执行。在整个执行的过程中,JVM 会用一块空间来存储程序执行期间需要用到的数据,这块空间一般被称为运行时数据区,也就是常说的 JVM 内存。 + +所以,当我们在谈 JVM 内存区域划分的时候,其实谈的就是这块空间——运行时数据区。 + +大家应该对官方出品的《Java 虚拟机规范》有所了解吧?了解这个规范可以让我们更深入地理解 JVM。该规范主要包含 6 个部分,分别是: + +- 第一章:引言 +- 第二章:Java 虚拟机结构 +- 第三章:Java 虚拟机编译 +- 第四章:[Class 文件](https://mp.weixin.qq.com/s/uMEZ2Xwctx4n-_8zvtDp5A) +- 第五章:加载、链接和初始化 +- 第六章:Java 虚拟机指令集 +- 第七章:操作码 + +根据第二章 Java 虚拟机结构中的规定,运行时数据区可以分为以下几个部分,见下图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/neicun-jiegou-e33179f3-275b-44c9-87f6-802198f8f360.png) + + +### 01、程序计数器 + +程序计数器(Program Counter Register)所占的内存空间不大,很小一块,可以看作是当前线程所执行的字节码指令的行号指示器。字节码解释器会在工作的时候改变这个计数器的值来选取下一条需要执行的字节码指令,像分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。 + +在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间的,因此,在任一具体时刻,一个 CPU 的内核只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,并且不能互相干扰,否则就会影响到程序的正常执行次序。 + +也就是说,我们要求程序计数器是线程私有的。 + +《Java 虚拟机规范》中规定,如果线程执行的是非本地(native)方法,则程序计数器中保存的是当前需要执行的指令地址;如果线程执行的是本地方法,则程序计数器中的值是 undefined。 + +为什么本地方法在程序计数器中的值是 undefined 的?因为本地方法大多是通过 C/C++ 实现的,并未编译成需要执行的字节码指令。 + +由于程序计数器中存储的数据所占的空间不会随程序的执行而发生大小上的改变,因此,程序计数器是不会发生内存溢出现象(OutOfMemory)的。 + +### 02、Java 虚拟机栈 + +Java 虚拟机栈中是一个个栈帧,每个栈帧对应一个被调用的方法。当线程执行一个方法时,会创建一个对应的栈帧,并将栈帧压入栈中。当方法执行完毕后,将栈帧从栈中移除。[栈](https://mp.weixin.qq.com/s/fc48Z5tSMlBHweYIS1UL0g)遵循的是后进先出的原则,所以线程当前执行的方法对应的栈帧必定在 Java 虚拟机栈的顶部。 + +栈帧包含以下 5 个部分,见下图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/neicun-jiegou-4ea2a60a-05df-4ed1-8109-99ae23acefd1.png) + +[Java 虚拟机栈](https://tobebetterjavaer.com/jvm/how-jvm-run-zijiema-zhiling.md) + +### 04、堆 + +堆是所有线程共享的一块内存区域,在 Java 虚拟机启动的时候创建,用来存储对象(数组也是一种对象)。 + +以前,Java 中“几乎”所有的对象都会在堆中分配,但随着 JIT(Just-In-Time)编译器的发展和逃逸技术的逐渐成熟,所有的对象都分配到堆上渐渐变得不那么“绝对”了。从 JDK 7 开始,Java 虚拟机已经默认开启逃逸分析了,意味着如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。 + +简单解释一下 JIT 和逃逸分析。 + +常见的编译型语言如 C++,通常会把代码直接编译成 CPU 所能理解的机器码来运行。而 Java 为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由 javac 编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java 可能会干不过 C++ 这类编译型语言。 + +为了优化 Java 的性能 ,JVM 在解释器之外引入了 JIT 编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。 + +逃逸分析(Escape Analysis),简单来讲就是,Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。 + +堆是 Java 垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度来看,由于垃圾收集器基本都采用了分代垃圾收集的算法,所以堆还可以细分为:新生代和老年代。新生代还可以细分为:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。 + +堆这最容易出现的就是 OutOfMemoryError 错误,分为以下几种表现形式: + +- `OutOfMemoryError: GC Overhead Limit Exceeded`:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生该错误。 +- `java.lang.OutOfMemoryError: Java heap space`:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发该错误。和本机的物理内存无关,和我们配置的虚拟机内存大小有关! + +### 05、元空间 + +JDK 8 的时候,原有的方法区(更准确的说应该是永久代)被彻底移除,取而代之的是元空间。 + +我们来说说方法区吧。方法区和堆一样,是线程共享的区域,它用来存储已经被 Java 虚拟机加载的类信息、常量、静态变量,以及便器编译后的代码等。 + +在有些地方,方法区也被称为永久代。但其实不能这么理解。 + +>《Java 虚拟机规范》中只规定了有方法区这么一个概念和它的作用,并没有规定如何去实现它。那么不同的 Java 虚拟机可能就会有不同的实现。永久代是 HotSpot 对方法区的一种实现形式。也就是说,永久代只是 HotSpot 中的一个概念,而方法区则是 Java 虚拟机规范中的一个定义,一种规范。 + +换句话说,方法区和永久代的关系就像是 Java 中接口和类的关系,类实现了接口。 + +在方法区中,还有一块非常重要的部分,也就是运行时常量池。在讲 [class 文件](https://mp.weixin.qq.com/s/uMEZ2Xwctx4n-_8zvtDp5A)的时候,提到了每个 class 文件都会有个常量池,用来存放字符串常量、类和接口的名字、字段名、常量等等。运行时常量池和 class 文件的常量池是一一对应的,它就是通过 class 文件中的常量池来构建的。 + +JDK 7 之前,运行时常量池中包含着字符串常量池,都在方法区。 + +JDK 7 的时候,字符串常量池从方法区中拿出来放到了堆中,运行时常量池中的其他东西还在方法区中。 + +JDK 8 的时候,HotSpot 移除了永久代,也就是说方法区不存在了,取而代之的是元空间。也就意味着字符串常量池在堆中,运行时常量池跑到了元空间。 + +再来说说为什么要将永久代 (PermGen) 或者说方法区替换为元空间 (MetaSpace) 。 + +第一,永久代放在 Java 虚拟机中,就会受到 Java 虚拟机内存大小的限制,而元空间使用的是本地内存,也就脱离了 Java 虚拟机内存的限制。 + +第二,JDK 8 的时候,在 HotSpot 中融合了 JRockit 虚拟机,而 JRockit 中并没有永久代的概念,因此新的 HotSpot 就没有必要再开辟一块空间来作为永久代了。 + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/oom.md b/docs/jvm/oom.md new file mode 100644 index 0000000000..e6598b81a5 --- /dev/null +++ b/docs/jvm/oom.md @@ -0,0 +1,149 @@ +--- +title: 一次内存溢出排查优化实战 +shortTitle: 一次内存溢出排查优化实战 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,一次内存溢出排查优化实战 +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机,内存溢出 +--- + +# 一次内存溢出排查优化实战 + +## 前言 + +`OutOfMemoryError` 问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界、空指针等)来说这类问题是很难定位和解决的。 + +本文以最近碰到的一次线上内存溢出的定位、解决问题的方式展开;希望能对碰到类似问题的同学带来思路和帮助。 + +主要从`表现-->排查-->定位-->解决` 四个步骤来分析和解决问题。 + + + +## 表象 + +最近我们生产上的一个应用不断的爆出内存溢出,并且随着业务量的增长出现的频次越来越高。 + +该程序的业务逻辑非常简单,就是从 Kafka 中将数据消费下来然后批量的做持久化操作。 + +而现象则是随着 Kafka 的消息越多,出现的异常的频次就越快。由于当时还有其他工作所以只能让运维做重启,并且监控好堆内存以及 GC 情况。 + +> 重启大法虽好,可是依然不能根本解决问题。 + +## 排查 + +于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现问题。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-81051388-0c35-4de6-a3d9-4f546ef4bfec.jpg) + +结果发现老年代的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。 + +结合 jstat 的日志发现就算是发生了 FGC 老年代也已经回收不了,内存已经到顶。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-e79d4da0-fbb1-4918-a8d8-e29d2d64323b.jpg) + +甚至有几台应用 FGC 达到了上百次,时间也高的可怕。 + +这说明应用的内存使用肯定是有问题的,有许多赖皮对象始终回收不掉。 + +## 定位 + +由于生产上的内存 dump 文件非常大,达到了几十G。也是由于我们的内存设置太大有关。 + +所以导致想使用 MAT 分析需要花费大量时间。 + +因此我们便想是否可以在本地复现,这样就要好定位的多。 + +为了尽快的复现问题,我将本地应用最大堆内存设置为 150M。 + + +然后在消费 Kafka 那里 Mock 为一个 while 循环一直不断的生成数据。 + +同时当应用启动之后利用 VisualVM 连上应用实时监控内存、GC 的使用情况。 + +结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每产生一次 GC 内存都能有效的回收,所以这样并没有复现问题。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-4cf05af0-924f-406b-a8a4-5aa885e38cea.jpg) + + +没法复现问题就很难定位了。于是我们 review 代码,发现生产的逻辑和我们用 while 循环 Mock 数据还不太一样。 + +查看生产的日志发现每次从 Kafka 中取出的都是几百条数据,而我们 Mock 时每次只能产生**一条**。 + +为了尽可能的模拟生产情况便在服务器上跑着一个生产者程序,一直源源不断的向 Kafka 中发送数据。 + +果然不出意外只跑了一分多钟内存就顶不住了,观察左图发现 GC 的频次非常高,但是内存的回收却是相形见拙。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-a6d6c9cd-e79c-4a76-ba97-032cfefefd5f.jpg) + +同时后台也开始打印内存溢出了,这样便复现出问题。 + +## 解决 + +从目前的表现来看就是内存中有许多对象一直存在强引用关系导致得不到回收。 + +于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能可以立即 dump 出当前应用的内存情况。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-49b47ca3-b3e2-49f7-85c9-23f7a3ef6f93.jpg) + +结果发现 `com.lmax.disruptor.RingBuffer` 类型的对象占用了将近 50% 的内存。 + +看到这个包自然就想到了 `Disruptor` 环形队列。 + +再次 review 代码发现:从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。 + +这里也就能说明为什么第一次模拟数据没复现问题了。 + +模拟的时候是一个对象放进队列里,而生产的情况是 700 条数据放进队列里。这个数据量是 700 倍的差距。 + +而 Disruptor 作为一个环形队列,再对象没有被覆盖之前是一直存在的。 + +我也做了一个实验,证明确实如此。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-dee49da6-905a-4085-b82e-41e136d422e8.jpg) + +我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 HashMap 的取模定位)。 + +所以在生产上假设我们的队列大小是 1024,那么随着系统的运行最终肯定会导致 1024 个位置上装满了对象,而且每个位置是 700 个! + +于是查看了生产上 Disruptor 的 RingBuffer 配置,结果是:`1024*1024`。 + +这个数量级就非常吓人了。 + +为了验证是否是这个问题,我在本地将该值换为 2 ,一个最小值试试。 + +同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下: + +![](http://cdn.tobebetterjavaer.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) + +**你的点赞与转发是最大的支持。** + +原文链接:https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/ + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/problem-tools.md b/docs/jvm/problem-tools.md new file mode 100644 index 0000000000..d91cbffaee --- /dev/null +++ b/docs/jvm/problem-tools.md @@ -0,0 +1,568 @@ +--- +title: Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析) +shortTitle: Java问题诊断和排查工具 +category: + - Java核心 +tag: + - Java虚拟机 +description: Java程序员进阶之路,小白的零基础Java教程,从入门到进阶,Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析) +head: + - - meta + - name: keywords + content: Java,JavaSE,教程,Java程序员进阶之路,jvm,Java虚拟机 +--- + +# Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析) + + +## JDK自带的工具 + +在JDK的bin目录下有很多命令行工具: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-547b1b2c-9fb4-4d1d-9c72-013ec210f6a5.jpg) + +  我们可以看到各个工具的大小基本上都稳定在27kb左右,这个不是JDK开发团队刻意为之的,而是因为这些工具大多数是 `jdk\lib\tools.jar` 类库的一层薄包装而已,他们的主要功能代码是在tools类库中实现的。 + +命令行工具的好处是:当应用程序部署到生产环境后,无论是直接接触物理服务器还是远程telnet到服务器上都会受到限制。而借助tools.jar类库里面的接口,我们可以直接在应用程序中实现功能强大的监控分析功能。 + +### 常用命令: + +这里主要介绍如下几个工具: + +![](http://cdn.tobebetterjavaer.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命令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-2017daf6-832a-4673-b776-ad3380e47402.png) + +执行jps命令,会列出所有正在运行的java进程,其中jps命令也是一个java程序。前面的数字就是进程的id,这个id的作用非常大,后面会有相关介绍。 + +**jps -help:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-031be661-e47e-44f0-9e33-34368b187662.png) + +**jps -l** 输出应用程序main.class的完整package名或者应用程序jar文件完整路径名 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-0ccc96dc-8053-4222-9824-b116f02776a4.png) + +**jps -v** 输出传递给JVM的参数 + +![](http://cdn.tobebetterjavaer.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虚拟机每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-e80d0925-2dcf-4204-b46d-47312df2a673.png) + +**3、jmap** + +主要用于打印指定java进程的共享对象内存映射或堆内存细节。 + +**堆Dump是反映堆使用情况的内存镜像,其中主要包括系统信息、虚拟机属性、完整的线程Dump、所有类和对象的状态等。一般在内存不足,GC异常等情况下,我们会去怀疑内存泄漏,这个时候就会去打印堆Dump。** + +jmap的用法摘要: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-96a70bab-5cee-4068-8ccb-1d35124abeea.png) + +**1、`jmap pid`** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-38d5c9da-e433-43d2-b1bc-3f3634e05497.png) + +打印的信息分别为:共享对象的起始地址、映射大小、共享对象路径的全程。 + +**2、`jmap -heap pid`:查看堆使用情况** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-75acf4c8-393d-43d1-b208-04de1f0ba6bd.png) + +**3、`jmap -histo pid`:查看堆中对象数量和大小** + +![](http://cdn.tobebetterjavaer.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 -