diff --git a/docs/notes/Docker.md b/docs/notes/Docker.md
index 36c95c92..701d53ca 100644
--- a/docs/notes/Docker.md
+++ b/docs/notes/Docker.md
@@ -1,83 +1,95 @@
-# 一、解决的问题
+
+* [一、解决的问题](#一解决的问题)
+* [二、与虚拟机的比较](#二与虚拟机的比较)
+* [三、优势](#三优势)
+* [四、使用场景](#四使用场景)
+* [五、镜像与容器](#五镜像与容器)
+* [参考资料](#参考资料)
+
+
+
+# 一、解决的问题
由于不同的机器有不同的操作系统,以及不同的库和组件,在将一个应用部署到多台机器上需要进行大量的环境配置操作。
-Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其它机器上。
+Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其它机器上。
-
+
-# 二、与虚拟机的比较
+# 二、与虚拟机的比较
-虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。
+虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。
-
+
-## 启动速度
+## 启动速度
启动虚拟机需要先启动虚拟机的操作系统,再启动应用,这个过程非常慢;
-而启动 Docker 相当于启动宿主操作系统上的一个进程。
+而启动 Docker 相当于启动宿主操作系统上的一个进程。
-## 占用资源
+## 占用资源
-虚拟机是一个完整的操作系统,需要占用大量的磁盘、内存和 CPU 资源,一台机器只能开启几十个的虚拟机。
+虚拟机是一个完整的操作系统,需要占用大量的磁盘、内存和 CPU 资源,一台机器只能开启几十个的虚拟机。
-而 Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。
+而 Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。
-# 三、优势
+# 三、优势
-除了启动速度快以及占用资源少之外,Docker 具有以下优势:
+除了启动速度快以及占用资源少之外,Docker 具有以下优势:
-## 更容易迁移
+## 更容易迁移
提供一致性的运行环境。已经打包好的应用可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。
-## 更容易维护
+## 更容易维护
使用分层技术和镜像,使得应用可以更容易复用重复的部分。复用程度越高,维护工作也越容易。
-## 更容易扩展
+## 更容易扩展
可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像可以非常容易得到我们想要的镜像。
-# 四、使用场景
+# 四、使用场景
-## 持续集成
+## 持续集成
持续集成指的是频繁地将代码集成到主干上,这样能够更快地发现错误。
-Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。
+Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。
-## 提供可伸缩的云服务
+## 提供可伸缩的云服务
-根据应用的负载情况,可以很容易地增加或者减少 Docker。
+根据应用的负载情况,可以很容易地增加或者减少 Docker。
-## 搭建微服务架构
+## 搭建微服务架构
-Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。
+Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。
-# 五、镜像与容器
+# 五、镜像与容器
镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。
-镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。
+镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。
-构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。
+构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。
-
+
-# 参考资料
+# 参考资料
-- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/)
-- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html)
-- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php)
-- [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/)
-- [理解 Docker(2):Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html)
-- [为什么要使用 Docker?](https://yeasy.gitbooks.io/docker_practice/introduction/why.html)
-- [What is Docker](https://www.docker.com/what-docker)
-- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)
+- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/)
+- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html)
+- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php)
+- [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/)
+- [理解 Docker(2):Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html)
+- [为什么要使用 Docker?](https://yeasy.gitbooks.io/docker_practice/introduction/why.html)
+- [What is Docker](https://www.docker.com/what-docker)
+- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)
----bottom---CyC---
-
-
-
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Git.md b/docs/notes/Git.md
index d155b982..47ac0888 100644
--- a/docs/notes/Git.md
+++ b/docs/notes/Git.md
@@ -1,8 +1,24 @@
-# 集中式与分布式
+
+* [集中式与分布式](#集中式与分布式)
+* [中心服务器](#中心服务器)
+* [工作流](#工作流)
+* [分支实现](#分支实现)
+* [冲突](#冲突)
+* [Fast forward](#fast-forward)
+* [分支管理策略](#分支管理策略)
+* [储藏(Stashing)](#储藏stashing)
+* [SSH 传输设置](#ssh-传输设置)
+* [.gitignore 文件](#gitignore-文件)
+* [Git 命令一览](#git-命令一览)
+* [参考资料](#参考资料)
+
-Git 属于分布式版本控制系统,而 SVN 属于集中式。
-
+# 集中式与分布式
+
+Git 属于分布式版本控制系统,而 SVN 属于集中式。
+
+
集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。
@@ -12,133 +28,139 @@ Git 属于分布式版本控制系统,而 SVN 属于集中式。
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
-# 中心服务器
+# 中心服务器
-中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。
+中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。
-Github 就是一个中心服务器。
+Github 就是一个中心服务器。
-# 工作流
+# 工作流
-新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。
+新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。
-Git 的版本库有一个称为 Stage 的暂存区以及最后的 History 版本库,History 中存有所有分支,使用一个 HEAD 指针指向当前分支。
+Git 的版本库有一个称为 Stage 的暂存区以及最后的 History 版本库,History 中存有所有分支,使用一个 HEAD 指针指向当前分支。
-
+
-- git add files 把文件的修改添加到暂存区
-- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
-- git reset -- files 使用当前分支上的修改覆盖暂存区,用来撤销最后一次 git add files
-- git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改
+- git add files 把文件的修改添加到暂存区
+- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
+- git reset -- files 使用当前分支上的修改覆盖暂存区,用来撤销最后一次 git add files
+- git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改
-
+
可以跳过暂存区域直接从分支中取出修改,或者直接提交修改到分支中。
-- git commit -a 直接把所有文件的修改添加到暂存区然后执行提交
-- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
+- git commit -a 直接把所有文件的修改添加到暂存区然后执行提交
+- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
-
+
-# 分支实现
+# 分支实现
-使用指针将每个提交连接成一条时间线,HEAD 指针指向当前分支指针。
+使用指针将每个提交连接成一条时间线,HEAD 指针指向当前分支指针。
-
+
-新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
+新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
-
+
每次提交只会让当前分支指针向前移动,而其它分支指针不会移动。
-
+
合并分支也只需要改变指针即可。
-
+
-# 冲突
+# 冲突
当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。
-
+
-Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
+Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
```
-<<<<<<< HEAD
-Creating a new branch is quick & simple.
+<<<<<<< HEAD
+Creating a new branch is quick & simple.
=======
-Creating a new branch is quick AND simple.
->>>>>>> feature1
+Creating a new branch is quick AND simple.
+>>>>>>> feature1
```
-# Fast forward
+# Fast forward
-"快进式合并"(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
+"快进式合并"(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
-可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
+可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
```
-$ git merge --no-ff -m "merge with no-ff" dev
+$ git merge --no-ff -m "merge with no-ff" dev
```
-
+
-# 分支管理策略
+# 分支管理策略
-master 分支应该是非常稳定的,只用来发布新版本;
+master 分支应该是非常稳定的,只用来发布新版本;
-日常开发在开发分支 dev 上进行。
+日常开发在开发分支 dev 上进行。
-
+
-# 储藏(Stashing)
+# 储藏(Stashing)
在一个分支上操作之后,如果还没有将修改提交到分支上,此时进行切换分支,那么另一个分支上也能看到新的修改。这是因为所有分支都共用一个工作区的缘故。
-可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈上,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
+可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈上,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
```
-$ git stash
-Saved working directory and index state \ "WIP on master: 049d078 added the index file"
-HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
+$ git stash
+Saved working directory and index state \ "WIP on master: 049d078 added the index file"
+HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
```
-该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
+该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
-# SSH 传输设置
+# SSH 传输设置
-Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
+Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
-如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:
+如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:
```
-$ ssh-keygen -t rsa -C "youremail@example.com"
+$ ssh-keygen -t rsa -C "youremail@example.com"
```
-然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。
+然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。
-# .gitignore 文件
+# .gitignore 文件
忽略以下文件:
-- 操作系统自动生成的文件,比如缩略图;
-- 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
-- 自己的敏感信息,比如存放口令的配置文件。
+- 操作系统自动生成的文件,比如缩略图;
+- 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
+- 自己的敏感信息,比如存放口令的配置文件。
-不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
+不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
-# Git 命令一览
+# Git 命令一览
-
+
比较详细的地址:http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf
-# 参考资料
+# 参考资料
-- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
-- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
-- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
-- [Learn Git Branching](https://learngitbranching.js.org/)
+- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
+- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
+- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
+- [Learn Git Branching](https://learngitbranching.js.org/)
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/HTTP.md b/docs/notes/HTTP.md
index 2b2e8c44..b00979b8 100644
--- a/docs/notes/HTTP.md
+++ b/docs/notes/HTTP.md
@@ -1,838 +1,885 @@
-# 一 、基础概念
+
+* [一 、基础概念](#一-基础概念)
+ * [URI](#uri)
+ * [请求和响应报文](#请求和响应报文)
+* [二、HTTP 方法](#二http-方法)
+ * [GET](#get)
+ * [HEAD](#head)
+ * [POST](#post)
+ * [PUT](#put)
+ * [PATCH](#patch)
+ * [DELETE](#delete)
+ * [OPTIONS](#options)
+ * [CONNECT](#connect)
+ * [TRACE](#trace)
+* [三、HTTP 状态码](#三http-状态码)
+ * [1XX 信息](#1xx-信息)
+ * [2XX 成功](#2xx-成功)
+ * [3XX 重定向](#3xx-重定向)
+ * [4XX 客户端错误](#4xx-客户端错误)
+ * [5XX 服务器错误](#5xx-服务器错误)
+* [四、HTTP 首部](#四http-首部)
+ * [通用首部字段](#通用首部字段)
+ * [请求首部字段](#请求首部字段)
+ * [响应首部字段](#响应首部字段)
+ * [实体首部字段](#实体首部字段)
+* [五、具体应用](#五具体应用)
+ * [连接管理](#连接管理)
+ * [Cookie](#cookie)
+ * [缓存](#缓存)
+ * [内容协商](#内容协商)
+ * [内容编码](#内容编码)
+ * [范围请求](#范围请求)
+ * [分块传输编码](#分块传输编码)
+ * [多部分对象集合](#多部分对象集合)
+ * [虚拟主机](#虚拟主机)
+ * [通信数据转发](#通信数据转发)
+* [六、HTTPS](#六https)
+ * [加密](#加密)
+ * [认证](#认证)
+ * [完整性保护](#完整性保护)
+ * [HTTPS 的缺点](#https-的缺点)
+* [七、HTTP/2.0](#七http20)
+ * [HTTP/1.x 缺陷](#http1x-缺陷)
+ * [二进制分帧层](#二进制分帧层)
+ * [服务端推送](#服务端推送)
+ * [首部压缩](#首部压缩)
+* [八、HTTP/1.1 新特性](#八http11-新特性)
+* [九、GET 和 POST 比较](#九get-和-post-比较)
+ * [作用](#作用)
+ * [参数](#参数)
+ * [安全](#安全)
+ * [幂等性](#幂等性)
+ * [可缓存](#可缓存)
+ * [XMLHttpRequest](#xmlhttprequest)
+* [参考资料](#参考资料)
+
-## URI
-URI 包含 URL 和 URN。
+# 一 、基础概念
-
+## URI
-## 请求和响应报文
+URI 包含 URL 和 URN。
-### 1. 请求报文
+
-
+## 请求和响应报文
-### 2. 响应报文
+### 1. 请求报文
-
+
-# 二、HTTP 方法
+### 2. 响应报文
-客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。
+
-## GET
+# 二、HTTP 方法
-> 获取资源
+客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。
-当前网络请求中,绝大部分使用的是 GET 方法。
+## GET
-## HEAD
+> 获取资源
-> 获取报文首部
+当前网络请求中,绝大部分使用的是 GET 方法。
-和 GET 方法类似,但是不返回报文实体主体部分。
+## HEAD
-主要用于确认 URL 的有效性以及资源更新的日期时间等。
+> 获取报文首部
-## POST
+和 GET 方法类似,但是不返回报文实体主体部分。
-> 传输实体主体
+主要用于确认 URL 的有效性以及资源更新的日期时间等。
-POST 主要用来传输数据,而 GET 主要用来获取资源。
+## POST
-更多 POST 与 GET 的比较请见第九章。
+> 传输实体主体
-## PUT
+POST 主要用来传输数据,而 GET 主要用来获取资源。
-> 上传文件
+更多 POST 与 GET 的比较请见第九章。
+
+## PUT
+
+> 上传文件
由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。
```html
-PUT /new.html HTTP/1.1
-Host: example.com
-Content-type: text/html
-Content-length: 16
+PUT /new.html HTTP/1.1
+Host: example.com
+Content-type: text/html
+Content-length: 16
-New File
+New File
```
-## PATCH
+## PATCH
-> 对资源进行部分修改
+> 对资源进行部分修改
-PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。
+PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。
```html
-PATCH /file.txt HTTP/1.1
-Host: www.example.com
-Content-Type: application/example
-If-Match: "e0023aa4e"
-Content-Length: 100
+PATCH /file.txt HTTP/1.1
+Host: www.example.com
+Content-Type: application/example
+If-Match: "e0023aa4e"
+Content-Length: 100
-[description of changes]
+[description of changes]
```
-## DELETE
+## DELETE
-> 删除文件
+> 删除文件
-与 PUT 功能相反,并且同样不带验证机制。
+与 PUT 功能相反,并且同样不带验证机制。
```html
-DELETE /file.html HTTP/1.1
+DELETE /file.html HTTP/1.1
```
-## OPTIONS
+## OPTIONS
-> 查询支持的方法
+> 查询支持的方法
-查询指定的 URL 能够支持的方法。
+查询指定的 URL 能够支持的方法。
-会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容。
+会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容。
-## CONNECT
+## CONNECT
-> 要求在与代理服务器通信时建立隧道
+> 要求在与代理服务器通信时建立隧道
-使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
+使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
```html
-CONNECT www.example.com:443 HTTP/1.1
+CONNECT www.example.com:443 HTTP/1.1
```
-
+
-## TRACE
+## TRACE
-> 追踪路径
+> 追踪路径
服务器会将通信路径返回给客户端。
-发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。
+发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。
-通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪)。
+通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪)。
-# 三、HTTP 状态码
+# 三、HTTP 状态码
-服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
+服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
-| 状态码 | 类别 | 含义 |
-| :---: | :---: | :---: |
-| 1XX | Informational(信息性状态码) | 接收的请求正在处理 |
-| 2XX | Success(成功状态码) | 请求正常处理完毕 |
-| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
-| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
-| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
+| 状态码 | 类别 | 含义 |
+| :---: | :---: | :---: |
+| 1XX | Informational(信息性状态码) | 接收的请求正在处理 |
+| 2XX | Success(成功状态码) | 请求正常处理完毕 |
+| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
+| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
+| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
-## 1XX 信息
+## 1XX 信息
-- **100 Continue**:表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。
+- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。
-## 2XX 成功
+## 2XX 成功
-- **200 OK**
+- **200 OK**
-- **204 No Content**:请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
+- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
-- **206 Partial Content**:表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。
+- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。
-## 3XX 重定向
+## 3XX 重定向
-- **301 Moved Permanently**:永久性重定向
+- **301 Moved Permanently** :永久性重定向
-- **302 Found**:临时性重定向
+- **302 Found** :临时性重定向
-- **303 See Other**:和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
+- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
-- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
+- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
-- **304 Not Modified**:如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
+- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
-- **307 Temporary Redirect**:临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
+- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
-## 4XX 客户端错误
+## 4XX 客户端错误
-- **400 Bad Request**:请求报文中存在语法错误。
+- **400 Bad Request** :请求报文中存在语法错误。
-- **401 Unauthorized**:该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
+- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
-- **403 Forbidden**:请求被拒绝。
+- **403 Forbidden** :请求被拒绝。
-- **404 Not Found**
+- **404 Not Found**
-## 5XX 服务器错误
+## 5XX 服务器错误
-- **500 Internal Server Error**:服务器正在执行请求时发生错误。
+- **500 Internal Server Error** :服务器正在执行请求时发生错误。
-- **503 Service Unavailable**:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
+- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
-# 四、HTTP 首部
+# 四、HTTP 首部
-有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。
+有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。
各种首部字段及其含义如下(不需要全记,仅供查阅):
-## 通用首部字段
+## 通用首部字段
-| 首部字段名 | 说明 |
-| :--: | :--: |
-| Cache-Control | 控制缓存的行为 |
-| Connection | 控制不再转发给代理的首部字段、管理持久连接|
-| Date | 创建报文的日期时间 |
-| Pragma | 报文指令 |
-| Trailer | 报文末端的首部一览 |
-| Transfer-Encoding | 指定报文主体的传输编码方式 |
-| Upgrade | 升级为其他协议 |
-| Via | 代理服务器的相关信息 |
-| Warning | 错误通知 |
+| 首部字段名 | 说明 |
+| :--: | :--: |
+| Cache-Control | 控制缓存的行为 |
+| Connection | 控制不再转发给代理的首部字段、管理持久连接|
+| Date | 创建报文的日期时间 |
+| Pragma | 报文指令 |
+| Trailer | 报文末端的首部一览 |
+| Transfer-Encoding | 指定报文主体的传输编码方式 |
+| Upgrade | 升级为其他协议 |
+| Via | 代理服务器的相关信息 |
+| Warning | 错误通知 |
-## 请求首部字段
+## 请求首部字段
-| 首部字段名 | 说明 |
-| :--: | :--: |
-| Accept | 用户代理可处理的媒体类型 |
-| Accept-Charset | 优先的字符集 |
-| Accept-Encoding | 优先的内容编码 |
-| Accept-Language | 优先的语言(自然语言) |
-| Authorization | Web 认证信息 |
-| Expect | 期待服务器的特定行为 |
-| From | 用户的电子邮箱地址 |
-| Host | 请求资源所在服务器 |
-| If-Match | 比较实体标记(ETag) |
-| If-Modified-Since | 比较资源的更新时间 |
-| If-None-Match | 比较实体标记(与 If-Match 相反) |
-| If-Range | 资源未更新时发送实体 Byte 的范围请求 |
-| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) |
-| Max-Forwards | 最大传输逐跳数 |
-| Proxy-Authorization | 代理服务器要求客户端的认证信息 |
-| Range | 实体的字节范围请求 |
-| Referer | 对请求中 URI 的原始获取方 |
-| TE | 传输编码的优先级 |
-| User-Agent | HTTP 客户端程序的信息 |
+| 首部字段名 | 说明 |
+| :--: | :--: |
+| Accept | 用户代理可处理的媒体类型 |
+| Accept-Charset | 优先的字符集 |
+| Accept-Encoding | 优先的内容编码 |
+| Accept-Language | 优先的语言(自然语言) |
+| Authorization | Web 认证信息 |
+| Expect | 期待服务器的特定行为 |
+| From | 用户的电子邮箱地址 |
+| Host | 请求资源所在服务器 |
+| If-Match | 比较实体标记(ETag) |
+| If-Modified-Since | 比较资源的更新时间 |
+| If-None-Match | 比较实体标记(与 If-Match 相反) |
+| If-Range | 资源未更新时发送实体 Byte 的范围请求 |
+| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) |
+| Max-Forwards | 最大传输逐跳数 |
+| Proxy-Authorization | 代理服务器要求客户端的认证信息 |
+| Range | 实体的字节范围请求 |
+| Referer | 对请求中 URI 的原始获取方 |
+| TE | 传输编码的优先级 |
+| User-Agent | HTTP 客户端程序的信息 |
-## 响应首部字段
+## 响应首部字段
-| 首部字段名 | 说明 |
-| :--: | :--: |
-| Accept-Ranges | 是否接受字节范围请求 |
-| Age | 推算资源创建经过时间 |
-| ETag | 资源的匹配信息 |
-| Location | 令客户端重定向至指定 URI |
-| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
-| Retry-After | 对再次发起请求的时机要求 |
-| Server | HTTP 服务器的安装信息 |
-| Vary | 代理服务器缓存的管理信息 |
-| WWW-Authenticate | 服务器对客户端的认证信息 |
+| 首部字段名 | 说明 |
+| :--: | :--: |
+| Accept-Ranges | 是否接受字节范围请求 |
+| Age | 推算资源创建经过时间 |
+| ETag | 资源的匹配信息 |
+| Location | 令客户端重定向至指定 URI |
+| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
+| Retry-After | 对再次发起请求的时机要求 |
+| Server | HTTP 服务器的安装信息 |
+| Vary | 代理服务器缓存的管理信息 |
+| WWW-Authenticate | 服务器对客户端的认证信息 |
-## 实体首部字段
+## 实体首部字段
-| 首部字段名 | 说明 |
-| :--: | :--: |
-| Allow | 资源可支持的 HTTP 方法 |
-| Content-Encoding | 实体主体适用的编码方式 |
-| Content-Language | 实体主体的自然语言 |
-| Content-Length | 实体主体的大小 |
-| Content-Location | 替代对应资源的 URI |
-| Content-MD5 | 实体主体的报文摘要 |
-| Content-Range | 实体主体的位置范围 |
-| Content-Type | 实体主体的媒体类型 |
-| Expires | 实体主体过期的日期时间 |
-| Last-Modified | 资源的最后修改日期时间 |
+| 首部字段名 | 说明 |
+| :--: | :--: |
+| Allow | 资源可支持的 HTTP 方法 |
+| Content-Encoding | 实体主体适用的编码方式 |
+| Content-Language | 实体主体的自然语言 |
+| Content-Length | 实体主体的大小 |
+| Content-Location | 替代对应资源的 URI |
+| Content-MD5 | 实体主体的报文摘要 |
+| Content-Range | 实体主体的位置范围 |
+| Content-Type | 实体主体的媒体类型 |
+| Expires | 实体主体过期的日期时间 |
+| Last-Modified | 资源的最后修改日期时间 |
-# 五、具体应用
+# 五、具体应用
-## 连接管理
+## 连接管理
-
+
-### 1. 短连接与长连接
+### 1. 短连接与长连接
-当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。
+当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。
-长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
+长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
-- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`;
-- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`。
+- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`;
+- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`。
-### 2. 流水线
+### 2. 流水线
-默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
+默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。
-## Cookie
+## Cookie
-HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
+HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
-Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。
+Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。
-Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。
+Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。
-### 1. 用途
+### 1. 用途
-- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
-- 个性化设置(如用户自定义设置、主题等)
-- 浏览器行为跟踪(如跟踪分析用户行为等)
+- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
+- 个性化设置(如用户自定义设置、主题等)
+- 浏览器行为跟踪(如跟踪分析用户行为等)
-### 2. 创建过程
+### 2. 创建过程
-服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
+服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
```html
-HTTP/1.0 200 OK
-Content-type: text/html
-Set-Cookie: yummy_cookie=choco
-Set-Cookie: tasty_cookie=strawberry
+HTTP/1.0 200 OK
+Content-type: text/html
+Set-Cookie: yummy_cookie=choco
+Set-Cookie: tasty_cookie=strawberry
-[page content]
+[page content]
```
-客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。
+客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。
```html
-GET /sample_page.html HTTP/1.1
-Host: www.example.org
-Cookie: yummy_cookie=choco; tasty_cookie=strawberry
+GET /sample_page.html HTTP/1.1
+Host: www.example.org
+Cookie: yummy_cookie=choco; tasty_cookie=strawberry
```
-### 3. 分类
+### 3. 分类
-- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
-- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。
+- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
+- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。
```html
-Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
+Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
```
-### 4. 作用域
+### 4. 作用域
-Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。
+Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。
-Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:
+Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:
-- /docs
-- /docs/Web/
-- /docs/Web/HTTP
+- /docs
+- /docs/Web/
+- /docs/Web/HTTP
-### 5. JavaScript
+### 5. JavaScript
-通过 `document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。
+通过 `document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。
```html
-document.cookie = "yummy_cookie=choco";
-document.cookie = "tasty_cookie=strawberry";
+document.cookie = "yummy_cookie=choco";
+document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
```
-### 6. HttpOnly
+### 6. HttpOnly
-标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 `document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。
+标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 `document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。
```html
-Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
+Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
```
-### 7. Secure
+### 7. Secure
-标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。
+标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。
-### 8. Session
+### 8. Session
-除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。
+除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。
-Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。
+Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。
-使用 Session 维护用户登录状态的过程如下:
+使用 Session 维护用户登录状态的过程如下:
-- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
-- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID;
-- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
-- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。
+- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
+- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID;
+- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
+- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。
-应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
+应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
-### 9. 浏览器禁用 Cookie
+### 9. 浏览器禁用 Cookie
-此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。
+此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。
-### 10. Cookie 与 Session 选择
+### 10. Cookie 与 Session 选择
-- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时首选 Session;
-- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
-- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。
+- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时首选 Session;
+- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
+- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。
-## 缓存
+## 缓存
-### 1. 优点
+### 1. 优点
-- 缓解服务器压力;
-- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存。
+- 缓解服务器压力;
+- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存。
-### 2. 实现方法
+### 2. 实现方法
-- 让代理服务器进行缓存;
-- 让客户端浏览器进行缓存。
+- 让代理服务器进行缓存;
+- 让客户端浏览器进行缓存。
-### 3. Cache-Control
+### 3. Cache-Control
-HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。
+HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。
-**3.1 禁止进行缓存**
+**3.1 禁止进行缓存**
-no-store 指令规定不能对请求或响应的任何一部分进行缓存。
+no-store 指令规定不能对请求或响应的任何一部分进行缓存。
```html
-Cache-Control: no-store
+Cache-Control: no-store
```
-**3.2 强制确认缓存**
+**3.2 强制确认缓存**
-no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。
+no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。
```html
-Cache-Control: no-cache
+Cache-Control: no-cache
```
-**3.3 私有缓存和公共缓存**
+**3.3 私有缓存和公共缓存**
-private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。
+private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。
```html
-Cache-Control: private
+Cache-Control: private
```
-public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。
+public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。
```html
-Cache-Control: public
+Cache-Control: public
```
-**3.4 缓存过期机制**
+**3.4 缓存过期机制**
-max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
+max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
-max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。
+max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。
```html
-Cache-Control: max-age=31536000
+Cache-Control: max-age=31536000
```
-Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。
+Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。
```html
-Expires: Wed, 04 Jul 2012 08:26:05 GMT
+Expires: Wed, 04 Jul 2012 08:26:05 GMT
```
-- 在 HTTP/1.1 中,会优先处理 max-age 指令;
-- 在 HTTP/1.0 中,max-age 指令会被忽略掉。
+- 在 HTTP/1.1 中,会优先处理 max-age 指令;
+- 在 HTTP/1.0 中,max-age 指令会被忽略掉。
-### 4. 缓存验证
+### 4. 缓存验证
-需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。
+需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。
```html
-ETag: "82e22293907ce725faf67773957acd12"
+ETag: "82e22293907ce725faf67773957acd12"
```
-可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。
+可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。
```html
-If-None-Match: "82e22293907ce725faf67773957acd12"
+If-None-Match: "82e22293907ce725faf67773957acd12"
```
-Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应。
+Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应。
```html
-Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
+Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
```
```html
-If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
+If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
```
-## 内容协商
+## 内容协商
通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。
-### 1. 类型
+### 1. 类型
-**1.1 服务端驱动型**
+**1.1 服务端驱动型**
-客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language,服务器根据这些字段返回特定的资源。
+客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language,服务器根据这些字段返回特定的资源。
它存在以下问题:
-- 服务器很难知道客户端浏览器的全部信息;
-- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术);
-- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
+- 服务器很难知道客户端浏览器的全部信息;
+- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术);
+- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
-**1.2 代理驱动型**
+**1.2 代理驱动型**
-服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。
+服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。
-### 2. Vary
+### 2. Vary
```html
-Vary: Accept-Language
+Vary: Accept-Language
```
在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器请求该资源。
-例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。
+例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。
-## 内容编码
+## 内容编码
内容编码将实体主体进行压缩,从而减少传输的数据量。
常用的内容编码有:gzip、compress、deflate、identity。
-浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应的 Vary 首部至少要包含 Content-Encoding。
+浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应的 Vary 首部至少要包含 Content-Encoding。
-## 范围请求
+## 范围请求
如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而避免服务器重新发送所有数据。
-### 1. Range
+### 1. Range
-在请求报文中添加 Range 首部字段指定请求的范围。
+在请求报文中添加 Range 首部字段指定请求的范围。
```html
-GET /z4d4kWk.jpg HTTP/1.1
-Host: i.imgur.com
-Range: bytes=0-1023
+GET /z4d4kWk.jpg HTTP/1.1
+Host: i.imgur.com
+Range: bytes=0-1023
```
-请求成功的话服务器返回的响应包含 206 Partial Content 状态码。
+请求成功的话服务器返回的响应包含 206 Partial Content 状态码。
```html
-HTTP/1.1 206 Partial Content
-Content-Range: bytes 0-1023/146515
-Content-Length: 1024
+HTTP/1.1 206 Partial Content
+Content-Range: bytes 0-1023/146515
+Content-Length: 1024
...
-(binary content)
+(binary content)
```
-### 2. Accept-Ranges
+### 2. Accept-Ranges
-响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。
+响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。
```html
-Accept-Ranges: bytes
+Accept-Ranges: bytes
```
-### 3. 响应状态码
+### 3. 响应状态码
-- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
-- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。
-- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。
+- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
+- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。
+- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。
-## 分块传输编码
+## 分块传输编码
-Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。
+Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。
-## 多部分对象集合
+## 多部分对象集合
-一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。
+一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。
例如,上传多个表单时可以使用如下方式:
```html
-Content-Type: multipart/form-data; boundary=AaB03x
+Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
-Content-Disposition: form-data; name="submit-name"
+Content-Disposition: form-data; name="submit-name"
Larry
--AaB03x
-Content-Disposition: form-data; name="files"; filename="file1.txt"
-Content-Type: text/plain
+Content-Disposition: form-data; name="files"; filename="file1.txt"
+Content-Type: text/plain
-... contents of file1.txt ...
+... contents of file1.txt ...
--AaB03x--
```
-## 虚拟主机
+## 虚拟主机
-HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
+HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
-## 通信数据转发
+## 通信数据转发
-### 1. 代理
+### 1. 代理
代理服务器接受客户端的请求,并且转发给其它服务器。
使用代理的主要目的是:
-- 缓存
-- 负载均衡
-- 网络访问控制
-- 访问日志记录
+- 缓存
+- 负载均衡
+- 网络访问控制
+- 访问日志记录
代理服务器分为正向代理和反向代理两种:
-- 用户察觉得到正向代理的存在。
+- 用户察觉得到正向代理的存在。
-
+
-- 而反向代理一般位于内部网络中,用户察觉不到。
+- 而反向代理一般位于内部网络中,用户察觉不到。
-
+
-### 2. 网关
+### 2. 网关
-与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。
+与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。
-### 3. 隧道
+### 3. 隧道
-使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。
+使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。
-# 六、HTTPS
+# 六、HTTPS
-HTTP 有以下安全性问题:
+HTTP 有以下安全性问题:
-- 使用明文进行通信,内容可能会被窃听;
-- 不验证通信方的身份,通信方的身份有可能遭遇伪装;
-- 无法证明报文的完整性,报文有可能遭篡改。
+- 使用明文进行通信,内容可能会被窃听;
+- 不验证通信方的身份,通信方的身份有可能遭遇伪装;
+- 无法证明报文的完整性,报文有可能遭篡改。
-HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。
+HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。
-通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
+通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
-
+
-## 加密
+## 加密
-### 1. 对称密钥加密
+### 1. 对称密钥加密
-对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。
+对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。
-- 优点:运算速度快;
-- 缺点:无法安全地将密钥传输给通信方。
+- 优点:运算速度快;
+- 缺点:无法安全地将密钥传输给通信方。
-
+
-### 2.非对称密钥加密
+### 2.非对称密钥加密
-非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。
+非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。
公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。
-- 优点:可以更安全地将公开密钥传输给通信发送方;
-- 缺点:运算速度慢。
+- 优点:可以更安全地将公开密钥传输给通信发送方;
+- 缺点:运算速度慢。
-
+
-### 3. HTTPS 采用的加密方式
+### 3. HTTPS 采用的加密方式
-HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥)
+HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥)
-
+
-## 认证
+## 认证
-通过使用 **证书** 来对通信方进行认证。
+通过使用 **证书** 来对通信方进行认证。
-数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。
+数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。
-服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
+服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
-进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
+进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
-
+
-## 完整性保护
+## 完整性保护
-SSL 提供报文摘要功能来进行完整性保护。
+SSL 提供报文摘要功能来进行完整性保护。
-HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。
+HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。
-HTTPS 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。
+HTTPS 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。
-## HTTPS 的缺点
+## HTTPS 的缺点
-- 因为需要进行加密解密等过程,因此速度会更慢;
-- 需要支付证书授权的高额费用。
+- 因为需要进行加密解密等过程,因此速度会更慢;
+- 需要支付证书授权的高额费用。
-# 七、HTTP/2.0
+# 七、HTTP/2.0
-## HTTP/1.x 缺陷
+## HTTP/1.x 缺陷
-HTTP/1.x 实现简单是以牺牲性能为代价的:
+HTTP/1.x 实现简单是以牺牲性能为代价的:
-- 客户端需要使用多个连接才能实现并发和缩短延迟;
-- 不会压缩请求和响应首部,从而导致不必要的网络流量;
-- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。
+- 客户端需要使用多个连接才能实现并发和缩短延迟;
+- 不会压缩请求和响应首部,从而导致不必要的网络流量;
+- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。
-## 二进制分帧层
+## 二进制分帧层
-HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。
+HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。
-
+
-在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。
+在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。
-- 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。
-- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。
-- 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
+- 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。
+- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。
+- 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
-
+
-## 服务端推送
+## 服务端推送
-HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。
+HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。
-
+
-## 首部压缩
+## 首部压缩
-HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。
+HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。
-HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。
+HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。
-不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。
+不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。
-
+
-# 八、HTTP/1.1 新特性
+# 八、HTTP/1.1 新特性
详细内容请见上文
-- 默认是长连接
-- 支持流水线
-- 支持同时打开多个 TCP 连接
-- 支持虚拟主机
-- 新增状态码 100
-- 支持分块传输编码
-- 新增缓存处理指令 max-age
+- 默认是长连接
+- 支持流水线
+- 支持同时打开多个 TCP 连接
+- 支持虚拟主机
+- 新增状态码 100
+- 支持分块传输编码
+- 新增缓存处理指令 max-age
-# 九、GET 和 POST 比较
+# 九、GET 和 POST 比较
-## 作用
+## 作用
-GET 用于获取资源,而 POST 用于传输实体主体。
+GET 用于获取资源,而 POST 用于传输实体主体。
-## 参数
+## 参数
-GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。
+GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。
-因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参考支持标准字符集。
+因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参考支持标准字符集。
```
-GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
+GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
```
```
-POST /test/demo_form.asp HTTP/1.1
-Host: w3schools.com
+POST /test/demo_form.asp HTTP/1.1
+Host: w3schools.com
name1=value1&name2=value2
```
-## 安全
+## 安全
-安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
+安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
-GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。
+GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。
-安全的方法除了 GET 之外还有:HEAD、OPTIONS。
+安全的方法除了 GET 之外还有:HEAD、OPTIONS。
-不安全的方法除了 POST 之外还有 PUT、DELETE。
+不安全的方法除了 POST 之外还有 PUT、DELETE。
-## 幂等性
+## 幂等性
-幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。
+幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。
所有的安全方法也都是幂等的。
-在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。
+在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。
-GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:
+GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:
```
-GET /pageX HTTP/1.1
-GET /pageX HTTP/1.1
-GET /pageX HTTP/1.1
-GET /pageX HTTP/1.1
+GET /pageX HTTP/1.1
+GET /pageX HTTP/1.1
+GET /pageX HTTP/1.1
+GET /pageX HTTP/1.1
```
-POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录:
+POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录:
```
-POST /add_row HTTP/1.1 -> Adds a 1nd row
-POST /add_row HTTP/1.1 -> Adds a 2nd row
-POST /add_row HTTP/1.1 -> Adds a 3rd row
+POST /add_row HTTP/1.1 -> Adds a 1nd row
+POST /add_row HTTP/1.1 -> Adds a 2nd row
+POST /add_row HTTP/1.1 -> Adds a 3rd row
```
-DELETE /idX/delete HTTP/1.1 是幂等的,即便不同的请求接收到的状态码不一样:
+DELETE /idX/delete HTTP/1.1 是幂等的,即便不同的请求接收到的状态码不一样:
```
-DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists
-DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted
-DELETE /idX/delete HTTP/1.1 -> Returns 404
+DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists
+DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted
+DELETE /idX/delete HTTP/1.1 -> Returns 404
```
-## 可缓存
+## 可缓存
如果要对响应进行缓存,需要满足以下条件:
-- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。
-- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
-- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。
+- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。
+- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
+- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。
-## XMLHttpRequest
+## XMLHttpRequest
-为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest:
+为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest:
-> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。
+> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。
-- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。
-- 而 GET 方法 Header 和 Data 会一起发送。
+- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。
+- 而 GET 方法 Header 和 Data 会一起发送。
-# 参考资料
+# 参考资料
-- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
-- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
-- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
-- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php)
-- [Difference between file URI and URL in java](http://java2db.com/java-io/how-to-get-and-the-difference-between-file-uri-and-url-in-java)
-- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement)
-- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html)
-- [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/)
-- [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html)
-- [Web-VPN: Secure Proxies with SPDY & Chrome](https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/)
-- [File:HTTP persistent connection.svg](http://en.wikipedia.org/wiki/File:HTTP_persistent_connection.svg)
-- [Proxy server](https://en.wikipedia.org/wiki/Proxy_server)
-- [What Is This HTTPS/SSL Thing And Why Should You Care?](https://www.x-cart.com/blog/what-is-https-and-ssl.html)
-- [What is SSL Offloading?](https://securebox.comodo.com/ssl-sniffing/ssl-offloading/)
-- [Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption](https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html)
-- [An Introduction to Mutual SSL Authentication](https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication)
-- [The Difference Between URLs and URIs](https://danielmiessler.com/study/url-uri/)
-- [Cookie 与 Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment)
-- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827)
-- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html)
-- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/)
-- [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn)
-- [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest)
-- [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/)
-- [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences)
-- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2)
-- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
----bottom---CyC---
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
+- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
+- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
+- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php)
+- [Difference between file URI and URL in java](http://java2db.com/java-io/how-to-get-and-the-difference-between-file-uri-and-url-in-java)
+- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement)
+- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html)
+- [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/)
+- [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html)
+- [Web-VPN: Secure Proxies with SPDY & Chrome](https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/)
+- [File:HTTP persistent connection.svg](http://en.wikipedia.org/wiki/File:HTTP_persistent_connection.svg)
+- [Proxy server](https://en.wikipedia.org/wiki/Proxy_server)
+- [What Is This HTTPS/SSL Thing And Why Should You Care?](https://www.x-cart.com/blog/what-is-https-and-ssl.html)
+- [What is SSL Offloading?](https://securebox.comodo.com/ssl-sniffing/ssl-offloading/)
+- [Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption](https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html)
+- [An Introduction to Mutual SSL Authentication](https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication)
+- [The Difference Between URLs and URIs](https://danielmiessler.com/study/url-uri/)
+- [Cookie 与 Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment)
+- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827)
+- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html)
+- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/)
+- [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn)
+- [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest)
+- [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/)
+- [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences)
+- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2)
+- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Java IO.md b/docs/notes/Java IO.md
index e5ccf766..30cfe07e 100644
--- a/docs/notes/Java IO.md
+++ b/docs/notes/Java IO.md
@@ -1,286 +1,319 @@
-# 一、概览
+
+* [一、概览](#一概览)
+* [二、磁盘操作](#二磁盘操作)
+* [三、字节操作](#三字节操作)
+ * [实现文件复制](#实现文件复制)
+ * [装饰者模式](#装饰者模式)
+* [四、字符操作](#四字符操作)
+ * [编码与解码](#编码与解码)
+ * [String 的编码方式](#string-的编码方式)
+ * [Reader 与 Writer](#reader-与-writer)
+ * [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
+* [五、对象操作](#五对象操作)
+ * [序列化](#序列化)
+ * [Serializable](#serializable)
+ * [transient](#transient)
+* [六、网络操作](#六网络操作)
+ * [InetAddress](#inetaddress)
+ * [URL](#url)
+ * [Sockets](#sockets)
+ * [Datagram](#datagram)
+* [七、NIO](#七nio)
+ * [流与块](#流与块)
+ * [通道与缓冲区](#通道与缓冲区)
+ * [缓冲区状态变量](#缓冲区状态变量)
+ * [文件 NIO 实例](#文件-nio-实例)
+ * [选择器](#选择器)
+ * [套接字 NIO 实例](#套接字-nio-实例)
+ * [内存映射文件](#内存映射文件)
+ * [对比](#对比)
+* [八、参考资料](#八参考资料)
+
-Java 的 I/O 大概可以分成以下几类:
-- 磁盘操作:File
-- 字节操作:InputStream 和 OutputStream
-- 字符操作:Reader 和 Writer
-- 对象操作:Serializable
-- 网络操作:Socket
-- 新的输入/输出:NIO
+# 一、概览
-# 二、磁盘操作
+Java 的 I/O 大概可以分成以下几类:
-File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
+- 磁盘操作:File
+- 字节操作:InputStream 和 OutputStream
+- 字符操作:Reader 和 Writer
+- 对象操作:Serializable
+- 网络操作:Socket
+- 新的输入/输出:NIO
+
+# 二、磁盘操作
+
+File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
递归地列出一个目录下所有文件:
```java
-public static void listAllFiles(File dir) {
- if (dir == null || !dir.exists()) {
- return;
- }
- if (dir.isFile()) {
- System.out.println(dir.getName());
- return;
- }
- for (File file : dir.listFiles()) {
- listAllFiles(file);
- }
+public static void listAllFiles(File dir) {
+ if (dir == null || !dir.exists()) {
+ return;
+ }
+ if (dir.isFile()) {
+ System.out.println(dir.getName());
+ return;
+ }
+ for (File file : dir.listFiles()) {
+ listAllFiles(file);
+ }
}
```
-从 Java7 开始,可以使用 Paths 和 Files 代替 File。
+从 Java7 开始,可以使用 Paths 和 Files 代替 File。
-# 三、字节操作
+# 三、字节操作
-## 实现文件复制
+## 实现文件复制
```java
-public static void copyFile(String src, String dist) throws IOException {
- FileInputStream in = new FileInputStream(src);
- FileOutputStream out = new FileOutputStream(dist);
+public static void copyFile(String src, String dist) throws IOException {
+ FileInputStream in = new FileInputStream(src);
+ FileOutputStream out = new FileOutputStream(dist);
- byte[] buffer = new byte[20 * 1024];
- int cnt;
+ byte[] buffer = new byte[20 * 1024];
+ int cnt;
- // read() 最多读取 buffer.length 个字节
- // 返回的是实际读取的个数
- // 返回 -1 的时候表示读到 eof,即文件尾
- while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
- out.write(buffer, 0, cnt);
- }
+ // read() 最多读取 buffer.length 个字节
+ // 返回的是实际读取的个数
+ // 返回 -1 的时候表示读到 eof,即文件尾
+ while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
+ out.write(buffer, 0, cnt);
+ }
- in.close();
- out.close();
+ in.close();
+ out.close();
}
```
-## 装饰者模式
+## 装饰者模式
-Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
+Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
-- InputStream 是抽象组件;
-- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
-- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
+- InputStream 是抽象组件;
+- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
+- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
-
+
-实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
+实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
```java
-FileInputStream fileInputStream = new FileInputStream(filePath);
-BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
+FileInputStream fileInputStream = new FileInputStream(filePath);
+BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
```
-DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
+DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
-# 四、字符操作
+# 四、字符操作
-## 编码与解码
+## 编码与解码
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
-- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
-- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
-- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
+- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
+- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
+- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
-UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
+UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
-Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
+Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
-## String 的编码方式
+## String 的编码方式
-String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
+String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
```java
-String str1 = "中文";
-byte[] bytes = str1.getBytes("UTF-8");
-String str2 = new String(bytes, "UTF-8");
+String str1 = "中文";
+byte[] bytes = str1.getBytes("UTF-8");
+String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
```
-在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处,因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
+在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处,因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
```java
-byte[] bytes = str1.getBytes();
+byte[] bytes = str1.getBytes();
```
-## Reader 与 Writer
+## Reader 与 Writer
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
-- InputStreamReader 实现从字节流解码成字符流;
-- OutputStreamWriter 实现字符流编码成为字节流。
+- InputStreamReader 实现从字节流解码成字符流;
+- OutputStreamWriter 实现字符流编码成为字节流。
-## 实现逐行输出文本文件的内容
+## 实现逐行输出文本文件的内容
```java
-public static void readFileContent(String filePath) throws IOException {
+public static void readFileContent(String filePath) throws IOException {
- FileReader fileReader = new FileReader(filePath);
- BufferedReader bufferedReader = new BufferedReader(fileReader);
+ FileReader fileReader = new FileReader(filePath);
+ BufferedReader bufferedReader = new BufferedReader(fileReader);
- String line;
- while ((line = bufferedReader.readLine()) != null) {
- System.out.println(line);
- }
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ System.out.println(line);
+ }
- // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
- // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
- // 因此只要一个 close() 调用即可
- bufferedReader.close();
+ // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
+ // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
+ // 因此只要一个 close() 调用即可
+ bufferedReader.close();
}
```
-# 五、对象操作
+# 五、对象操作
-## 序列化
+## 序列化
序列化就是将一个对象转换成字节序列,方便存储和传输。
-- 序列化:ObjectOutputStream.writeObject()
-- 反序列化:ObjectInputStream.readObject()
+- 序列化:ObjectOutputStream.writeObject()
+- 反序列化:ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
-## Serializable
+## Serializable
-序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
+序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
```java
-public static void main(String[] args) throws IOException, ClassNotFoundException {
+public static void main(String[] args) throws IOException, ClassNotFoundException {
- A a1 = new A(123, "abc");
- String objectFile = "file/a1";
+ A a1 = new A(123, "abc");
+ String objectFile = "file/a1";
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
- objectOutputStream.writeObject(a1);
- objectOutputStream.close();
+ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
+ objectOutputStream.writeObject(a1);
+ objectOutputStream.close();
- ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
- A a2 = (A) objectInputStream.readObject();
- objectInputStream.close();
- System.out.println(a2);
+ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
+ A a2 = (A) objectInputStream.readObject();
+ objectInputStream.close();
+ System.out.println(a2);
}
-private static class A implements Serializable {
+private static class A implements Serializable {
- private int x;
- private String y;
+ private int x;
+ private String y;
- A(int x, String y) {
- this.x = x;
- this.y = y;
- }
+ A(int x, String y) {
+ this.x = x;
+ this.y = y;
+ }
- @Override
- public String toString() {
- return "x = " + x + " " + "y = " + y;
- }
+ @Override
+ public String toString() {
+ return "x = " + x + " " + "y = " + y;
+ }
}
```
-## transient
+## transient
-transient 关键字可以使一些属性不会被序列化。
+transient 关键字可以使一些属性不会被序列化。
-ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
+ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
```java
-private transient Object[] elementData;
+private transient Object[] elementData;
```
-# 六、网络操作
+# 六、网络操作
-Java 中的网络支持:
+Java 中的网络支持:
-- InetAddress:用于表示网络上的硬件资源,即 IP 地址;
-- URL:统一资源定位符;
-- Sockets:使用 TCP 协议实现网络通信;
-- Datagram:使用 UDP 协议实现网络通信。
+- InetAddress:用于表示网络上的硬件资源,即 IP 地址;
+- URL:统一资源定位符;
+- Sockets:使用 TCP 协议实现网络通信;
+- Datagram:使用 UDP 协议实现网络通信。
-## InetAddress
+## InetAddress
没有公有的构造函数,只能通过静态方法来创建实例。
```java
-InetAddress.getByName(String host);
-InetAddress.getByAddress(byte[] address);
+InetAddress.getByName(String host);
+InetAddress.getByAddress(byte[] address);
```
-## URL
+## URL
-可以直接从 URL 中读取字节流数据。
+可以直接从 URL 中读取字节流数据。
```java
-public static void main(String[] args) throws IOException {
+public static void main(String[] args) throws IOException {
- URL url = new URL("http://www.baidu.com");
+ URL url = new URL("http://www.baidu.com");
- /* 字节流 */
- InputStream is = url.openStream();
+ /* 字节流 */
+ InputStream is = url.openStream();
- /* 字符流 */
- InputStreamReader isr = new InputStreamReader(is, "utf-8");
+ /* 字符流 */
+ InputStreamReader isr = new InputStreamReader(is, "utf-8");
- /* 提供缓存功能 */
- BufferedReader br = new BufferedReader(isr);
+ /* 提供缓存功能 */
+ BufferedReader br = new BufferedReader(isr);
- String line;
- while ((line = br.readLine()) != null) {
- System.out.println(line);
- }
+ String line;
+ while ((line = br.readLine()) != null) {
+ System.out.println(line);
+ }
- br.close();
+ br.close();
}
```
-## Sockets
+## Sockets
-- ServerSocket:服务器端类
-- Socket:客户端类
-- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
+- ServerSocket:服务器端类
+- Socket:客户端类
+- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
-
+
-## Datagram
+## Datagram
-- DatagramSocket:通信类
-- DatagramPacket:数据包类
+- DatagramSocket:通信类
+- DatagramPacket:数据包类
-# 七、NIO
+# 七、NIO
-新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
+新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
-## 流与块
+## 流与块
-I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
+I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
-面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
+面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
-面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
+面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
-I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
+I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
-## 通道与缓冲区
+## 通道与缓冲区
-### 1. 通道
+### 1. 通道
-通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
+通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
-通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
+通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
-- FileChannel:从文件中读写数据;
-- DatagramChannel:通过 UDP 读写网络中数据;
-- SocketChannel:通过 TCP 读写网络中数据;
-- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
+- FileChannel:从文件中读写数据;
+- DatagramChannel:通过 UDP 读写网络中数据;
+- SocketChannel:通过 TCP 读写网络中数据;
+- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
-### 2. 缓冲区
+### 2. 缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
@@ -288,308 +321,306 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基
缓冲区包括以下类型:
-- ByteBuffer
-- CharBuffer
-- ShortBuffer
-- IntBuffer
-- LongBuffer
-- FloatBuffer
-- DoubleBuffer
+- ByteBuffer
+- CharBuffer
+- ShortBuffer
+- IntBuffer
+- LongBuffer
+- FloatBuffer
+- DoubleBuffer
-## 缓冲区状态变量
+## 缓冲区状态变量
-- capacity:最大容量;
-- position:当前已经读写的字节数;
-- limit:还可以读写的字节数。
+- capacity:最大容量;
+- position:当前已经读写的字节数;
+- limit:还可以读写的字节数。
状态变量的改变过程举例:
-① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
+① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
-
+
-② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
+② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
-
+
-③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
+③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
-
+
-④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
+④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
-
+
-⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
+⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
-
+
-## 文件 NIO 实例
+## 文件 NIO 实例
-以下展示了使用 NIO 快速复制文件的实例:
+以下展示了使用 NIO 快速复制文件的实例:
```java
-public static void fastCopy(String src, String dist) throws IOException {
+public static void fastCopy(String src, String dist) throws IOException {
- /* 获得源文件的输入字节流 */
- FileInputStream fin = new FileInputStream(src);
+ /* 获得源文件的输入字节流 */
+ FileInputStream fin = new FileInputStream(src);
- /* 获取输入字节流的文件通道 */
- FileChannel fcin = fin.getChannel();
+ /* 获取输入字节流的文件通道 */
+ FileChannel fcin = fin.getChannel();
- /* 获取目标文件的输出字节流 */
- FileOutputStream fout = new FileOutputStream(dist);
+ /* 获取目标文件的输出字节流 */
+ FileOutputStream fout = new FileOutputStream(dist);
- /* 获取输出字节流的文件通道 */
- FileChannel fcout = fout.getChannel();
+ /* 获取输出字节流的文件通道 */
+ FileChannel fcout = fout.getChannel();
- /* 为缓冲区分配 1024 个字节 */
- ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
+ /* 为缓冲区分配 1024 个字节 */
+ ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
- while (true) {
+ while (true) {
- /* 从输入通道中读取数据到缓冲区中 */
- int r = fcin.read(buffer);
+ /* 从输入通道中读取数据到缓冲区中 */
+ int r = fcin.read(buffer);
- /* read() 返回 -1 表示 EOF */
- if (r == -1) {
- break;
- }
+ /* read() 返回 -1 表示 EOF */
+ if (r == -1) {
+ break;
+ }
- /* 切换读写 */
- buffer.flip();
+ /* 切换读写 */
+ buffer.flip();
- /* 把缓冲区的内容写入输出文件中 */
- fcout.write(buffer);
+ /* 把缓冲区的内容写入输出文件中 */
+ fcout.write(buffer);
- /* 清空缓冲区 */
- buffer.clear();
- }
+ /* 清空缓冲区 */
+ buffer.clear();
+ }
}
```
-## 选择器
+## 选择器
-NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
+NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
-NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
+NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
-通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
+通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
-因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
+因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
-应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
+应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
-
+
-### 1. 创建选择器
+### 1. 创建选择器
```java
-Selector selector = Selector.open();
+Selector selector = Selector.open();
```
-### 2. 将通道注册到选择器上
+### 2. 将通道注册到选择器上
```java
-ServerSocketChannel ssChannel = ServerSocketChannel.open();
+ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
-ssChannel.register(selector, SelectionKey.OP_ACCEPT);
+ssChannel.register(selector, SelectionKey.OP_ACCEPT);
```
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
-- SelectionKey.OP_CONNECT
-- SelectionKey.OP_ACCEPT
-- SelectionKey.OP_READ
-- SelectionKey.OP_WRITE
+- SelectionKey.OP_CONNECT
+- SelectionKey.OP_ACCEPT
+- SelectionKey.OP_READ
+- SelectionKey.OP_WRITE
-它们在 SelectionKey 的定义如下:
+它们在 SelectionKey 的定义如下:
```java
-public static final int OP_READ = 1 << 0;
-public static final int OP_WRITE = 1 << 2;
-public static final int OP_CONNECT = 1 << 3;
-public static final int OP_ACCEPT = 1 << 4;
+public static final int OP_READ = 1 << 0;
+public static final int OP_WRITE = 1 << 2;
+public static final int OP_CONNECT = 1 << 3;
+public static final int OP_ACCEPT = 1 << 4;
```
可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:
```java
-int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
+int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
-### 3. 监听事件
+### 3. 监听事件
```java
-int num = selector.select();
+int num = selector.select();
```
-使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
+使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
-### 4. 获取到达的事件
+### 4. 获取到达的事件
```java
-Set keys = selector.selectedKeys();
-Iterator keyIterator = keys.iterator();
-while (keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if (key.isAcceptable()) {
- // ...
- } else if (key.isReadable()) {
- // ...
- }
- keyIterator.remove();
+Set keys = selector.selectedKeys();
+Iterator keyIterator = keys.iterator();
+while (keyIterator.hasNext()) {
+ SelectionKey key = keyIterator.next();
+ if (key.isAcceptable()) {
+ // ...
+ } else if (key.isReadable()) {
+ // ...
+ }
+ keyIterator.remove();
}
```
-### 5. 事件循环
+### 5. 事件循环
-因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
+因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
```java
-while (true) {
- int num = selector.select();
- Set keys = selector.selectedKeys();
- Iterator keyIterator = keys.iterator();
- while (keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if (key.isAcceptable()) {
- // ...
- } else if (key.isReadable()) {
- // ...
- }
- keyIterator.remove();
- }
+while (true) {
+ int num = selector.select();
+ Set keys = selector.selectedKeys();
+ Iterator keyIterator = keys.iterator();
+ while (keyIterator.hasNext()) {
+ SelectionKey key = keyIterator.next();
+ if (key.isAcceptable()) {
+ // ...
+ } else if (key.isReadable()) {
+ // ...
+ }
+ keyIterator.remove();
+ }
}
```
-## 套接字 NIO 实例
+## 套接字 NIO 实例
```java
-public class NIOServer {
+public class NIOServer {
- public static void main(String[] args) throws IOException {
+ public static void main(String[] args) throws IOException {
- Selector selector = Selector.open();
+ Selector selector = Selector.open();
- ServerSocketChannel ssChannel = ServerSocketChannel.open();
- ssChannel.configureBlocking(false);
- ssChannel.register(selector, SelectionKey.OP_ACCEPT);
+ ServerSocketChannel ssChannel = ServerSocketChannel.open();
+ ssChannel.configureBlocking(false);
+ ssChannel.register(selector, SelectionKey.OP_ACCEPT);
- ServerSocket serverSocket = ssChannel.socket();
- InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
- serverSocket.bind(address);
+ ServerSocket serverSocket = ssChannel.socket();
+ InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
+ serverSocket.bind(address);
- while (true) {
+ while (true) {
- selector.select();
- Set keys = selector.selectedKeys();
- Iterator keyIterator = keys.iterator();
+ selector.select();
+ Set keys = selector.selectedKeys();
+ Iterator keyIterator = keys.iterator();
- while (keyIterator.hasNext()) {
+ while (keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
+ SelectionKey key = keyIterator.next();
- if (key.isAcceptable()) {
+ if (key.isAcceptable()) {
- ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
+ ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
- // 服务器会为每个新连接创建一个 SocketChannel
- SocketChannel sChannel = ssChannel1.accept();
- sChannel.configureBlocking(false);
+ // 服务器会为每个新连接创建一个 SocketChannel
+ SocketChannel sChannel = ssChannel1.accept();
+ sChannel.configureBlocking(false);
- // 这个新连接主要用于从客户端读取数据
- sChannel.register(selector, SelectionKey.OP_READ);
+ // 这个新连接主要用于从客户端读取数据
+ sChannel.register(selector, SelectionKey.OP_READ);
- } else if (key.isReadable()) {
+ } else if (key.isReadable()) {
- SocketChannel sChannel = (SocketChannel) key.channel();
- System.out.println(readDataFromSocketChannel(sChannel));
- sChannel.close();
- }
+ SocketChannel sChannel = (SocketChannel) key.channel();
+ System.out.println(readDataFromSocketChannel(sChannel));
+ sChannel.close();
+ }
- keyIterator.remove();
- }
- }
- }
+ keyIterator.remove();
+ }
+ }
+ }
- private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
+ private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- StringBuilder data = new StringBuilder();
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ StringBuilder data = new StringBuilder();
- while (true) {
+ while (true) {
- buffer.clear();
- int n = sChannel.read(buffer);
- if (n == -1) {
- break;
- }
- buffer.flip();
- int limit = buffer.limit();
- char[] dst = new char[limit];
- for (int i = 0; i < limit; i++) {
- dst[i] = (char) buffer.get(i);
- }
- data.append(dst);
- buffer.clear();
- }
- return data.toString();
- }
+ buffer.clear();
+ int n = sChannel.read(buffer);
+ if (n == -1) {
+ break;
+ }
+ buffer.flip();
+ int limit = buffer.limit();
+ char[] dst = new char[limit];
+ for (int i = 0; i < limit; i++) {
+ dst[i] = (char) buffer.get(i);
+ }
+ data.append(dst);
+ buffer.clear();
+ }
+ return data.toString();
+ }
}
```
```java
-public class NIOClient {
+public class NIOClient {
- public static void main(String[] args) throws IOException {
- Socket socket = new Socket("127.0.0.1", 8888);
- OutputStream out = socket.getOutputStream();
- String s = "hello world";
- out.write(s.getBytes());
- out.close();
- }
+ public static void main(String[] args) throws IOException {
+ Socket socket = new Socket("127.0.0.1", 8888);
+ OutputStream out = socket.getOutputStream();
+ String s = "hello world";
+ out.write(s.getBytes());
+ out.close();
+ }
}
```
-## 内存映射文件
+## 内存映射文件
-内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
+内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
-下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
+下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
```java
-MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
+MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
-## 对比
+## 对比
-NIO 与普通 I/O 的区别主要有以下两点:
+NIO 与普通 I/O 的区别主要有以下两点:
-- NIO 是非阻塞的;
-- NIO 面向块,I/O 面向流。
+- NIO 是非阻塞的;
+- NIO 面向块,I/O 面向流。
-# 八、参考资料
+# 八、参考资料
-- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
-- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
-- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
-- [Java NIO 浅析](https://tech.meituan.com/nio.html)
-- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
-- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
-- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
-- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
-- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
-- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
----bottom---CyC---
-
-
-
-
-
-
-
+- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
+- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
+- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
+- [Java NIO 浅析](https://tech.meituan.com/nio.html)
+- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
+- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
+- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
+- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
+- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
+- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Java 基础.md b/docs/notes/Java 基础.md
index 12bd38cf..ebde0866 100644
--- a/docs/notes/Java 基础.md
+++ b/docs/notes/Java 基础.md
@@ -1,24 +1,3 @@
-<<<<<<< HEAD
-# 一、数据类型
-
-## 基本类型
-
-- byte/8
-- char/16
-- short/16
-- int/32
-- float/32
-- long/64
-- double/64
-- boolean/~
-
-boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。
-
-- [Primitive Data Types](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html)
-- [The Java® Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf)
-
-## 包装类型
-=======
* [一、数据类型](#一数据类型)
* [基本类型](#基本类型)
@@ -80,600 +59,599 @@ boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是
- [The Java® Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf)
## 包装类型
->>>>>>> 9f680db0cc99bd992c7f979442ecf458a33f9c1b
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
```java
-Integer x = 2; // 装箱
-int y = x; // 拆箱
+Integer x = 2; // 装箱
+int y = x; // 拆箱
```
-## 缓存池
+## 缓存池
-new Integer(123) 与 Integer.valueOf(123) 的区别在于:
+new Integer(123) 与 Integer.valueOf(123) 的区别在于:
-- new Integer(123) 每次都会新建一个对象;
-- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
+- new Integer(123) 每次都会新建一个对象;
+- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
```java
-Integer x = new Integer(123);
-Integer y = new Integer(123);
-System.out.println(x == y); // false
-Integer z = Integer.valueOf(123);
-Integer k = Integer.valueOf(123);
-System.out.println(z == k); // true
+Integer x = new Integer(123);
+Integer y = new Integer(123);
+System.out.println(x == y); // false
+Integer z = Integer.valueOf(123);
+Integer k = Integer.valueOf(123);
+System.out.println(z == k); // true
```
-valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
+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);
+public static Integer valueOf(int i) {
+ if (i >= IntegerCache.low && i <= IntegerCache.high)
+ return IntegerCache.cache[i + (-IntegerCache.low)];
+ return new Integer(i);
}
```
-在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
+在 Java 8 中,Integer 缓存池的大小默认为 -128\~127。
```java
-static final int low = -128;
-static final int high;
-static final Integer cache[];
+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;
+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++);
+ 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;
+ // range [-128, 127] must be interned (JLS7 5.1.7)
+ assert IntegerCache.high >= 127;
}
```
-编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
+编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
```java
-Integer m = 123;
-Integer n = 123;
-System.out.println(m == n); // true
+Integer m = 123;
+Integer n = 123;
+System.out.println(m == n); // true
```
基本类型对应的缓冲池如下:
-- boolean values true and false
-- all byte values
-- short values between -128 and 127
-- int values between -128 and 127
-- char in the range \u0000 to \u007F
+- boolean values true and false
+- all byte values
+- short values between -128 and 127
+- int values between -128 and 127
+- char in the range \u0000 to \u007F
在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
-[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123
+[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123
](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123)
-# 二、String
+# 二、String
-## 概览
+## 概览
-String 被声明为 final,因此它不可被继承。
+String 被声明为 final,因此它不可被继承。
-在 Java 8 中,String 内部使用 char 数组存储数据。
+在 Java 8 中,String 内部使用 char 数组存储数据。
```java
-public final class String
- implements java.io.Serializable, Comparable, CharSequence {
- /** The value is used for character storage. */
- private final char value[];
+public final class String
+ implements java.io.Serializable, Comparable, CharSequence {
+ /** The value is used for character storage. */
+ private final char value[];
}
```
-在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码。
+在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码。
```java
-public final class String
- implements java.io.Serializable, Comparable, CharSequence {
- /** The value is used for character storage. */
- private final byte[] value;
+public final class String
+ implements java.io.Serializable, Comparable, CharSequence {
+ /** The value is used for character storage. */
+ private final byte[] value;
- /** The identifier of the encoding used to encode the bytes in {@code value}. */
- private final byte coder;
+ /** The identifier of the encoding used to encode the bytes in {@code value}. */
+ private final byte coder;
}
```
-value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
+value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
-## 不可变的好处
+## 不可变的好处
-**1. 可以缓存 hash 值**
+**1. 可以缓存 hash 值**
-因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
+因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
-**2. String Pool 的需要**
+**2. String Pool 的需要**
-如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
+如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
-
+
-**3. 安全性**
+**3. 安全性**
-String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
+String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
-**4. 线程安全**
+**4. 线程安全**
-String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
+String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
-[Program Creek : Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/)
+[Program Creek : Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/)
-## String, StringBuffer and StringBuilder
+## String, StringBuffer and StringBuilder
-**1. 可变性**
+**1. 可变性**
-- String 不可变
-- StringBuffer 和 StringBuilder 可变
+- String 不可变
+- StringBuffer 和 StringBuilder 可变
-**2. 线程安全**
+**2. 线程安全**
-- String 不可变,因此是线程安全的
-- StringBuilder 不是线程安全的
-- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
+- String 不可变,因此是线程安全的
+- StringBuilder 不是线程安全的
+- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
-[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder)
+[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder)
-## String Pool
+## String Pool
-字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。
+字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。
-当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
+当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
-下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。
+下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。
```java
-String s1 = new String("aaa");
-String s2 = new String("aaa");
-System.out.println(s1 == s2); // false
-String s3 = s1.intern();
-String s4 = s1.intern();
-System.out.println(s3 == s4); // true
+String s1 = new String("aaa");
+String s2 = new String("aaa");
+System.out.println(s1 == s2); // false
+String s3 = s1.intern();
+String s4 = s1.intern();
+System.out.println(s3 == s4); // true
```
-如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。
+如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。
```java
-String s5 = "bbb";
-String s6 = "bbb";
-System.out.println(s5 == s6); // true
+String s5 = "bbb";
+String s6 = "bbb";
+System.out.println(s5 == s6); // true
```
-在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
+在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
-- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning)
-- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html)
+- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning)
+- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html)
-## new String("abc")
+## new String("abc")
-使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。
+使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。
-- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
-- 而使用 new 的方式会在堆中创建一个字符串对象。
+- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
+- 而使用 new 的方式会在堆中创建一个字符串对象。
-创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。
+创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。
```java
-public class NewStringTest {
- public static void main(String[] args) {
- String s = new String("abc");
- }
+public class NewStringTest {
+ public static void main(String[] args) {
+ String s = new String("abc");
+ }
}
```
-使用 javap -verbose 进行反编译,得到以下内容:
+使用 javap -verbose 进行反编译,得到以下内容:
```java
-// ...
-Constant pool:
-// ...
- #2 = Class #18 // java/lang/String
- #3 = String #19 // abc
-// ...
- #18 = Utf8 java/lang/String
- #19 = Utf8 abc
-// ...
+// ...
+Constant pool:
+// ...
+ #2 = Class #18 // java/lang/String
+ #3 = String #19 // abc
+// ...
+ #18 = Utf8 java/lang/String
+ #19 = Utf8 abc
+// ...
- public static void main(java.lang.String[]);
- descriptor: ([Ljava/lang/String;)V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=3, locals=2, args_size=1
- 0: new #2 // class java/lang/String
- 3: dup
- 4: ldc #3 // String abc
- 6: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V
- 9: astore_1
-// ...
+ public static void main(java.lang.String[]);
+ descriptor: ([Ljava/lang/String;)V
+ flags: ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=3, locals=2, args_size=1
+ 0: new #2 // class java/lang/String
+ 3: dup
+ 4: ldc #3 // String abc
+ 6: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V
+ 9: astore_1
+// ...
```
-在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。
+在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。
-以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。
+以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。
```java
-public String(String original) {
- this.value = original.value;
- this.hash = original.hash;
+public String(String original) {
+ this.value = original.value;
+ this.hash = original.hash;
}
```
-# 三、运算
+# 三、运算
-## 参数传递
+## 参数传递
-Java 的参数是以值传递的形式传入方法中,而不是引用传递。
+Java 的参数是以值传递的形式传入方法中,而不是引用传递。
-以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。
+以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。
```java
-public class Dog {
+public class Dog {
- String name;
+ String name;
- Dog(String name) {
- this.name = name;
- }
+ Dog(String name) {
+ this.name = name;
+ }
- String getName() {
- return this.name;
- }
+ String getName() {
+ return this.name;
+ }
- void setName(String name) {
- this.name = name;
- }
+ void setName(String name) {
+ this.name = name;
+ }
- String getObjectAddress() {
- return super.toString();
- }
+ String getObjectAddress() {
+ return super.toString();
+ }
}
```
```java
-public class PassByValueExample {
- public static void main(String[] args) {
- Dog dog = new Dog("A");
- System.out.println(dog.getObjectAddress()); // Dog@4554617c
- func(dog);
- System.out.println(dog.getObjectAddress()); // Dog@4554617c
- System.out.println(dog.getName()); // A
- }
+public class PassByValueExample {
+ public static void main(String[] args) {
+ Dog dog = new Dog("A");
+ System.out.println(dog.getObjectAddress()); // Dog@4554617c
+ func(dog);
+ System.out.println(dog.getObjectAddress()); // Dog@4554617c
+ System.out.println(dog.getName()); // A
+ }
- private static void func(Dog dog) {
- System.out.println(dog.getObjectAddress()); // Dog@4554617c
- dog = new Dog("B");
- System.out.println(dog.getObjectAddress()); // Dog@74a14482
- System.out.println(dog.getName()); // B
- }
+ private static void func(Dog dog) {
+ System.out.println(dog.getObjectAddress()); // Dog@4554617c
+ dog = new Dog("B");
+ System.out.println(dog.getObjectAddress()); // Dog@74a14482
+ System.out.println(dog.getName()); // B
+ }
}
```
如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
```java
-class PassByValueExample {
- public static void main(String[] args) {
- Dog dog = new Dog("A");
- func(dog);
- System.out.println(dog.getName()); // B
- }
+class PassByValueExample {
+ public static void main(String[] args) {
+ Dog dog = new Dog("A");
+ func(dog);
+ System.out.println(dog.getName()); // B
+ }
- private static void func(Dog dog) {
- dog.setName("B");
- }
+ private static void func(Dog dog) {
+ dog.setName("B");
+ }
}
```
-[StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?](https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value)
+[StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?](https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value)
-## float 与 double
+## float 与 double
-Java 不能隐式执行向下转型,因为这会使得精度降低。
+Java 不能隐式执行向下转型,因为这会使得精度降低。
-1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。
+1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。
```java
-// float f = 1.1;
+// float f = 1.1;
```
-1.1f 字面量才是 float 类型。
+1.1f 字面量才是 float 类型。
```java
-float f = 1.1f;
+float f = 1.1f;
```
-## 隐式类型转换
+## 隐式类型转换
-因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。
+因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。
```java
-short s1 = 1;
-// s1 = s1 + 1;
+short s1 = 1;
+// s1 = s1 + 1;
```
-但是使用 += 或者 ++ 运算符可以执行隐式类型转换。
+但是使用 += 或者 ++ 运算符可以执行隐式类型转换。
```java
-s1 += 1;
-// s1++;
+s1 += 1;
+// s1++;
```
-上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:
+上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:
```java
-s1 = (short) (s1 + 1);
+s1 = (short) (s1 + 1);
```
-[StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?](https://stackoverflow.com/questions/8710619/why-dont-javas-compound-assignment-operators-require-casting)
+[StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?](https://stackoverflow.com/questions/8710619/why-dont-javas-compound-assignment-operators-require-casting)
-## switch
+## switch
-从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。
+从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。
```java
-String s = "a";
-switch (s) {
- case "a":
- System.out.println("aaa");
- break;
- case "b":
- System.out.println("bbb");
- break;
+String s = "a";
+switch (s) {
+ case "a":
+ System.out.println("aaa");
+ break;
+ case "b":
+ System.out.println("bbb");
+ break;
}
```
-switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
+switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
```java
-// long x = 111;
-// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
-// case 111:
-// System.out.println(111);
-// break;
-// case 222:
-// System.out.println(222);
-// break;
-// }
+// long x = 111;
+// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
+// case 111:
+// System.out.println(111);
+// break;
+// case 222:
+// System.out.println(222);
+// break;
+// }
```
-[StackOverflow : Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java)
+[StackOverflow : Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java)
-# 四、继承
+# 四、继承
-## 访问权限
+## 访问权限
-Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。
+Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。
可以对类或类中的成员(字段以及方法)加上访问修饰符。
-- 类可见表示其它类可以用这个类创建实例对象。
-- 成员可见表示其它类可以用这个类的实例对象访问到该成员;
+- 类可见表示其它类可以用这个类创建实例对象。
+- 成员可见表示其它类可以用这个类的实例对象访问到该成员;
-protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
+protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
-设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
+设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。
-字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。
+字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。
```java
-public class AccessExample {
- public String id;
+public class AccessExample {
+ public String id;
}
```
-可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。
+可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。
```java
-public class AccessExample {
+public class AccessExample {
- private int id;
+ private int id;
- public String getId() {
- return id + "";
- }
+ public String getId() {
+ return id + "";
+ }
- public void setId(String id) {
- this.id = Integer.valueOf(id);
- }
+ public void setId(String id) {
+ this.id = Integer.valueOf(id);
+ }
}
```
但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。
```java
-public class AccessWithInnerClassExample {
+public class AccessWithInnerClassExample {
- private class InnerClass {
- int x;
- }
+ private class InnerClass {
+ int x;
+ }
- private InnerClass innerClass;
+ private InnerClass innerClass;
- public AccessWithInnerClassExample() {
- innerClass = new InnerClass();
- }
+ public AccessWithInnerClassExample() {
+ innerClass = new InnerClass();
+ }
- public int getValue() {
- return innerClass.x; // 直接访问
- }
+ public int getValue() {
+ return innerClass.x; // 直接访问
+ }
}
```
-## 抽象类与接口
+## 抽象类与接口
-**1. 抽象类**
+**1. 抽象类**
-抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
+抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
```java
-public abstract class AbstractClassExample {
+public abstract class AbstractClassExample {
- protected int x;
- private int y;
+ protected int x;
+ private int y;
- public abstract void func1();
+ public abstract void func1();
- public void func2() {
- System.out.println("func2");
- }
+ public void func2() {
+ System.out.println("func2");
+ }
}
```
```java
-public class AbstractExtendClassExample extends AbstractClassExample {
- @Override
- public void func1() {
- System.out.println("func1");
- }
+public class AbstractExtendClassExample extends AbstractClassExample {
+ @Override
+ public void func1() {
+ System.out.println("func1");
+ }
}
```
```java
-// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
-AbstractClassExample ac2 = new AbstractExtendClassExample();
+// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
+AbstractClassExample ac2 = new AbstractExtendClassExample();
ac2.func1();
```
-**2. 接口**
+**2. 接口**
-接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
+接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
-从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
+从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
-接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
+接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
-接口的字段默认都是 static 和 final 的。
+接口的字段默认都是 static 和 final 的。
```java
-public interface InterfaceExample {
+public interface InterfaceExample {
- void func1();
+ void func1();
- default void func2(){
- System.out.println("func2");
- }
+ default void func2(){
+ System.out.println("func2");
+ }
- int x = 123;
- // int y; // Variable 'y' might not have been initialized
- public int z = 0; // Modifier 'public' is redundant for interface fields
- // private int k = 0; // Modifier 'private' not allowed here
- // protected int l = 0; // Modifier 'protected' not allowed here
- // private void fun3(); // Modifier 'private' not allowed here
+ int x = 123;
+ // int y; // Variable 'y' might not have been initialized
+ public int z = 0; // Modifier 'public' is redundant for interface fields
+ // private int k = 0; // Modifier 'private' not allowed here
+ // protected int l = 0; // Modifier 'protected' not allowed here
+ // private void fun3(); // Modifier 'private' not allowed here
}
```
```java
-public class InterfaceImplementExample implements InterfaceExample {
- @Override
- public void func1() {
- System.out.println("func1");
- }
+public class InterfaceImplementExample implements InterfaceExample {
+ @Override
+ public void func1() {
+ System.out.println("func1");
+ }
}
```
```java
-// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
-InterfaceExample ie2 = new InterfaceImplementExample();
+// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
+InterfaceExample ie2 = new InterfaceImplementExample();
ie2.func1();
System.out.println(InterfaceExample.x);
```
-**3. 比较**
+**3. 比较**
-- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
-- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
-- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
-- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
+- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
+- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
+- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
+- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
-**4. 使用选择**
+**4. 使用选择**
使用接口:
-- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
-- 需要使用多重继承。
+- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
+- 需要使用多重继承。
使用抽象类:
-- 需要在几个相关的类中共享代码。
-- 需要能控制继承来的成员的访问权限,而不是都为 public。
-- 需要继承非静态和非常量字段。
+- 需要在几个相关的类中共享代码。
+- 需要能控制继承来的成员的访问权限,而不是都为 public。
+- 需要继承非静态和非常量字段。
-在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
+在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
-- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
-- [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
+- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
+- [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
-## super
+## super
-- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
-- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
+- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
+- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
```java
-public class SuperExample {
+public class SuperExample {
- protected int x;
- protected int y;
+ protected int x;
+ protected int y;
- public SuperExample(int x, int y) {
- this.x = x;
- this.y = y;
- }
+ public SuperExample(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
- public void func() {
- System.out.println("SuperExample.func()");
- }
+ public void func() {
+ System.out.println("SuperExample.func()");
+ }
}
```
```java
-public class SuperExtendExample extends SuperExample {
+public class SuperExtendExample extends SuperExample {
- private int z;
+ private int z;
- public SuperExtendExample(int x, int y, int z) {
- super(x, y);
- this.z = z;
- }
+ public SuperExtendExample(int x, int y, int z) {
+ super(x, y);
+ this.z = z;
+ }
- @Override
- public void func() {
- super.func();
- System.out.println("SuperExtendExample.func()");
- }
+ @Override
+ public void func() {
+ super.func();
+ System.out.println("SuperExtendExample.func()");
+ }
}
```
```java
-SuperExample e = new SuperExtendExample(1, 2, 3);
+SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
```
@@ -682,42 +660,15 @@ SuperExample.func()
SuperExtendExample.func()
```
-[Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
+[Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
-## 重写与重载
+## 重写与重载
-**1. 重写(Override)**
+**1. 重写(Override)**
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
-为了满足里式替换原则,重写有以下三个限制:
-
-<<<<<<< HEAD
-使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
-
-下面的示例中,SubClass 为 SuperClass 的子类,SubClass 重写了 SuperClass 的 func() 方法。其中:
-
-- 子类方法访问权限为 public,大于父类的 protected。
-- 子类的返回类型为 ArrayList,是父类返回类型 List 的子类。
-- 子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。
-- 子类重写方法使用 @Override 注解,从而让编译器自动检查是否满足限制条件。
-
-```java
-class SuperClass {
- protected List func() throws Throwable {
- return new ArrayList<>();
- }
-}
-
-class SubClass extends SuperClass {
- @Override
- public ArrayList func() throws Exception {
- return new ArrayList<>();
- }
-=======
-- 子类方法的访问权限必须大于等于父类方法;
-- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
-- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
+为了满足里式替换原则,重写有有以下两个限制:
使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
@@ -740,239 +691,238 @@ class SubClass extends SuperClass {
public ArrayList func() throws Exception {
return new ArrayList<>();
}
->>>>>>> 9f680db0cc99bd992c7f979442ecf458a33f9c1b
}
```
-**2. 重载(Overload)**
+**2. 重载(Overload)**
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
-**3. 实例**
+**3. 实例**
```java
-class A {
- public String show(D obj) {
- return ("A and D");
- }
+class A {
+ public String show(D obj) {
+ return ("A and D");
+ }
- public String show(A obj) {
- return ("A and A");
- }
+ public String show(A obj) {
+ return ("A and A");
+ }
}
-class B extends A {
- public String show(B obj) {
- return ("B and B");
- }
+class B extends A {
+ public String show(B obj) {
+ return ("B and B");
+ }
- public String show(A obj) {
- return ("B and A");
- }
+ public String show(A obj) {
+ return ("B and A");
+ }
}
-class C extends B {
+class C extends B {
}
-class D extends B {
+class D extends B {
}
```
```java
-public class Test {
+public class Test {
- public static void main(String[] args) {
- A a1 = new A();
- A a2 = new B();
- B b = new B();
- C c = new C();
- D d = new D();
- System.out.println(a1.show(b)); // A and A
- System.out.println(a1.show(c)); // A and A
- System.out.println(a1.show(d)); // A and D
- System.out.println(a2.show(b)); // B and A
- System.out.println(a2.show(c)); // B and A
- System.out.println(a2.show(d)); // A and D
- System.out.println(b.show(b)); // B and B
- System.out.println(b.show(c)); // B and B
- System.out.println(b.show(d)); // A and D
- }
+ public static void main(String[] args) {
+ A a1 = new A();
+ A a2 = new B();
+ B b = new B();
+ C c = new C();
+ D d = new D();
+ System.out.println(a1.show(b)); // A and A
+ System.out.println(a1.show(c)); // A and A
+ System.out.println(a1.show(d)); // A and D
+ System.out.println(a2.show(b)); // B and A
+ System.out.println(a2.show(c)); // B and A
+ System.out.println(a2.show(d)); // A and D
+ System.out.println(b.show(b)); // B and B
+ System.out.println(b.show(c)); // B and B
+ System.out.println(b.show(d)); // A and D
+ }
}
```
涉及到重写时,方法调用的优先级为:
-- this.show(O)
-- super.show(O)
-- this.show((super)O)
-- super.show((super)O)
+- this.show(O)
+- super.show(O)
+- this.show((super)O)
+- super.show((super)O)
-# 五、Object 通用方法
+# 五、Object 通用方法
-## 概览
+## 概览
```java
-public native int hashCode()
+public native int hashCode()
-public boolean equals(Object obj)
+public boolean equals(Object obj)
-protected native Object clone() throws CloneNotSupportedException
+protected native Object clone() throws CloneNotSupportedException
-public String toString()
+public String toString()
-public final native Class> getClass()
+public final native Class> getClass()
-protected void finalize() throws Throwable {}
+protected void finalize() throws Throwable {}
-public final native void notify()
+public final native void notify()
-public final native void notifyAll()
+public final native void notifyAll()
-public final native void wait(long timeout) throws InterruptedException
+public final native void wait(long timeout) throws InterruptedException
-public final void wait(long timeout, int nanos) throws InterruptedException
+public final void wait(long timeout, int nanos) throws InterruptedException
-public final void wait() throws InterruptedException
+public final void wait() throws InterruptedException
```
-## equals()
+## equals()
-**1. 等价关系**
+**1. 等价关系**
-Ⅰ 自反性
+Ⅰ 自反性
```java
-x.equals(x); // true
+x.equals(x); // true
```
-Ⅱ 对称性
+Ⅱ 对称性
```java
-x.equals(y) == y.equals(x); // true
+x.equals(y) == y.equals(x); // true
```
-Ⅲ 传递性
+Ⅲ 传递性
```java
-if (x.equals(y) && y.equals(z))
- x.equals(z); // true;
+if (x.equals(y) && y.equals(z))
+ x.equals(z); // true;
```
-Ⅳ 一致性
+Ⅳ 一致性
-多次调用 equals() 方法结果不变
+多次调用 equals() 方法结果不变
```java
-x.equals(y) == x.equals(y); // true
+x.equals(y) == x.equals(y); // true
```
-Ⅴ 与 null 的比较
+Ⅴ 与 null 的比较
-对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
+对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
```java
-x.equals(null); // false;
+x.equals(null); // false;
```
-**2. 等价与相等**
+**2. 等价与相等**
-- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
-- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
+- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
+- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
```java
-Integer x = new Integer(1);
-Integer y = new Integer(1);
-System.out.println(x.equals(y)); // true
-System.out.println(x == y); // false
+Integer x = new Integer(1);
+Integer y = new Integer(1);
+System.out.println(x.equals(y)); // true
+System.out.println(x == y); // false
```
-**3. 实现**
+**3. 实现**
-- 检查是否为同一个对象的引用,如果是直接返回 true;
-- 检查是否是同一个类型,如果不是,直接返回 false;
-- 将 Object 对象进行转型;
-- 判断每个关键域是否相等。
+- 检查是否为同一个对象的引用,如果是直接返回 true;
+- 检查是否是同一个类型,如果不是,直接返回 false;
+- 将 Object 对象进行转型;
+- 判断每个关键域是否相等。
```java
-public class EqualExample {
+public class EqualExample {
- private int x;
- private int y;
- private int z;
+ private int x;
+ private int y;
+ private int z;
- public EqualExample(int x, int y, int z) {
- this.x = x;
- this.y = y;
- this.z = z;
- }
+ public EqualExample(int x, int y, int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
- EqualExample that = (EqualExample) o;
+ EqualExample that = (EqualExample) o;
- if (x != that.x) return false;
- if (y != that.y) return false;
- return z == that.z;
- }
+ if (x != that.x) return false;
+ if (y != that.y) return false;
+ return z == that.z;
+ }
}
```
-## hashCode()
+## hashCode()
-hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
+hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
-在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。
+在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。
-下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。
+下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。
```java
-EqualExample e1 = new EqualExample(1, 1, 1);
-EqualExample e2 = new EqualExample(1, 1, 1);
-System.out.println(e1.equals(e2)); // true
-HashSet set = new HashSet<>();
+EqualExample e1 = new EqualExample(1, 1, 1);
+EqualExample e2 = new EqualExample(1, 1, 1);
+System.out.println(e1.equals(e2)); // true
+HashSet set = new HashSet<>();
set.add(e1);
set.add(e2);
-System.out.println(set.size()); // 2
+System.out.println(set.size()); // 2
```
-理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
+理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
-一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,编译器会自动进行这个优化。
+一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,编译器会自动进行这个优化。
```java
@Override
-public int hashCode() {
- int result = 17;
- result = 31 * result + x;
- result = 31 * result + y;
- result = 31 * result + z;
- return result;
+public int hashCode() {
+ int result = 17;
+ result = 31 * result + x;
+ result = 31 * result + y;
+ result = 31 * result + z;
+ return result;
}
```
-## toString()
+## toString()
-默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
+默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
```java
-public class ToStringExample {
+public class ToStringExample {
- private int number;
+ private int number;
- public ToStringExample(int number) {
- this.number = number;
- }
+ public ToStringExample(int number) {
+ this.number = number;
+ }
}
```
```java
-ToStringExample example = new ToStringExample(123);
+ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
```
@@ -980,290 +930,290 @@ System.out.println(example.toString());
ToStringExample@4554617c
```
-## clone()
+## clone()
-**1. cloneable**
+**1. cloneable**
-clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
+clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
```java
-public class CloneExample {
- private int a;
- private int b;
+public class CloneExample {
+ private int a;
+ private int b;
}
```
```java
-CloneExample e1 = new CloneExample();
-// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
+CloneExample e1 = new CloneExample();
+// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
```
-重写 clone() 得到以下实现:
+重写 clone() 得到以下实现:
```java
-public class CloneExample {
- private int a;
- private int b;
+public class CloneExample {
+ private int a;
+ private int b;
- @Override
- public CloneExample clone() throws CloneNotSupportedException {
- return (CloneExample)super.clone();
- }
+ @Override
+ public CloneExample clone() throws CloneNotSupportedException {
+ return (CloneExample)super.clone();
+ }
}
```
```java
-CloneExample e1 = new CloneExample();
-try {
- CloneExample e2 = e1.clone();
-} catch (CloneNotSupportedException e) {
- e.printStackTrace();
+CloneExample e1 = new CloneExample();
+try {
+ CloneExample e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
}
```
```html
-java.lang.CloneNotSupportedException: CloneExample
+java.lang.CloneNotSupportedException: CloneExample
```
-以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。
+以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。
-应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
+应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
```java
-public class CloneExample implements Cloneable {
- private int a;
- private int b;
+public class CloneExample implements Cloneable {
+ private int a;
+ private int b;
- @Override
- public Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
}
```
-**2. 浅拷贝**
+**2. 浅拷贝**
拷贝对象和原始对象的引用类型引用同一个对象。
```java
-public class ShallowCloneExample implements Cloneable {
+public class ShallowCloneExample implements Cloneable {
- private int[] arr;
+ private int[] arr;
- public ShallowCloneExample() {
- arr = new int[10];
- for (int i = 0; i < arr.length; i++) {
- arr[i] = i;
- }
- }
+ public ShallowCloneExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
- public void set(int index, int value) {
- arr[index] = value;
- }
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
- public int get(int index) {
- return arr[index];
- }
+ public int get(int index) {
+ return arr[index];
+ }
- @Override
- protected ShallowCloneExample clone() throws CloneNotSupportedException {
- return (ShallowCloneExample) super.clone();
- }
+ @Override
+ protected ShallowCloneExample clone() throws CloneNotSupportedException {
+ return (ShallowCloneExample) super.clone();
+ }
}
```
```java
-ShallowCloneExample e1 = new ShallowCloneExample();
-ShallowCloneExample e2 = null;
-try {
- e2 = e1.clone();
-} catch (CloneNotSupportedException e) {
- e.printStackTrace();
+ShallowCloneExample e1 = new ShallowCloneExample();
+ShallowCloneExample e2 = null;
+try {
+ e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
}
-e1.set(2, 222);
-System.out.println(e2.get(2)); // 222
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 222
```
-**3. 深拷贝**
+**3. 深拷贝**
拷贝对象和原始对象的引用类型引用不同对象。
```java
-public class DeepCloneExample implements Cloneable {
+public class DeepCloneExample implements Cloneable {
- private int[] arr;
+ private int[] arr;
- public DeepCloneExample() {
- arr = new int[10];
- for (int i = 0; i < arr.length; i++) {
- arr[i] = i;
- }
- }
+ public DeepCloneExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
- public void set(int index, int value) {
- arr[index] = value;
- }
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
- public int get(int index) {
- return arr[index];
- }
+ public int get(int index) {
+ return arr[index];
+ }
- @Override
- protected DeepCloneExample clone() throws CloneNotSupportedException {
- DeepCloneExample result = (DeepCloneExample) super.clone();
- result.arr = new int[arr.length];
- for (int i = 0; i < arr.length; i++) {
- result.arr[i] = arr[i];
- }
- return result;
- }
+ @Override
+ protected DeepCloneExample clone() throws CloneNotSupportedException {
+ DeepCloneExample result = (DeepCloneExample) super.clone();
+ result.arr = new int[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ result.arr[i] = arr[i];
+ }
+ return result;
+ }
}
```
```java
-DeepCloneExample e1 = new DeepCloneExample();
-DeepCloneExample e2 = null;
-try {
- e2 = e1.clone();
-} catch (CloneNotSupportedException e) {
- e.printStackTrace();
+DeepCloneExample e1 = new DeepCloneExample();
+DeepCloneExample e2 = null;
+try {
+ e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
}
-e1.set(2, 222);
-System.out.println(e2.get(2)); // 2
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
```
-**4. clone() 的替代方案**
+**4. clone() 的替代方案**
-使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
+使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
```java
-public class CloneConstructorExample {
+public class CloneConstructorExample {
- private int[] arr;
+ private int[] arr;
- public CloneConstructorExample() {
- arr = new int[10];
- for (int i = 0; i < arr.length; i++) {
- arr[i] = i;
- }
- }
+ public CloneConstructorExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
- public CloneConstructorExample(CloneConstructorExample original) {
- arr = new int[original.arr.length];
- for (int i = 0; i < original.arr.length; i++) {
- arr[i] = original.arr[i];
- }
- }
+ public CloneConstructorExample(CloneConstructorExample original) {
+ arr = new int[original.arr.length];
+ for (int i = 0; i < original.arr.length; i++) {
+ arr[i] = original.arr[i];
+ }
+ }
- public void set(int index, int value) {
- arr[index] = value;
- }
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
- public int get(int index) {
- return arr[index];
- }
+ public int get(int index) {
+ return arr[index];
+ }
}
```
```java
-CloneConstructorExample e1 = new CloneConstructorExample();
-CloneConstructorExample e2 = new CloneConstructorExample(e1);
-e1.set(2, 222);
-System.out.println(e2.get(2)); // 2
+CloneConstructorExample e1 = new CloneConstructorExample();
+CloneConstructorExample e2 = new CloneConstructorExample(e1);
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
```
-# 六、关键字
+# 六、关键字
-## final
+## final
-**1. 数据**
+**1. 数据**
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
-- 对于基本类型,final 使数值不变;
-- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
+- 对于基本类型,final 使数值不变;
+- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
```java
-final int x = 1;
-// x = 2; // cannot assign value to final variable 'x'
-final A y = new A();
-y.a = 1;
+final int x = 1;
+// x = 2; // cannot assign value to final variable 'x'
+final A y = new A();
+y.a = 1;
```
-**2. 方法**
+**2. 方法**
声明方法不能被子类重写。
-private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
+private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
-**3. 类**
+**3. 类**
声明类不允许被继承。
-## static
+## static
-**1. 静态变量**
+**1. 静态变量**
-- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
-- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
+- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
+- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
```java
-public class A {
+public class A {
- private int x; // 实例变量
- private static int y; // 静态变量
+ private int x; // 实例变量
+ private static int y; // 静态变量
- public static void main(String[] args) {
- // int x = A.x; // Non-static field 'x' cannot be referenced from a static context
- A a = new A();
- int x = a.x;
- int y = A.y;
- }
+ public static void main(String[] args) {
+ // int x = A.x; // Non-static field 'x' cannot be referenced from a static context
+ A a = new A();
+ int x = a.x;
+ int y = A.y;
+ }
}
```
-**2. 静态方法**
+**2. 静态方法**
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
```java
-public abstract class A {
- public static void func1(){
- }
- // public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static'
+public abstract class A {
+ public static void func1(){
+ }
+ // public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static'
}
```
-只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。
+只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。
```java
-public class A {
+public class A {
- private static int x;
- private int y;
+ private static int x;
+ private int y;
- public static void func1(){
- int a = x;
- // int b = y; // Non-static field 'y' cannot be referenced from a static context
- // int b = this.y; // 'A.this' cannot be referenced from a static context
- }
+ public static void func1(){
+ int a = x;
+ // int b = y; // Non-static field 'y' cannot be referenced from a static context
+ // int b = this.y; // 'A.this' cannot be referenced from a static context
+ }
}
```
-**3. 静态语句块**
+**3. 静态语句块**
静态语句块在类初始化时运行一次。
```java
-public class A {
- static {
- System.out.println("123");
- }
+public class A {
+ static {
+ System.out.println("123");
+ }
- public static void main(String[] args) {
- A a1 = new A();
- A a2 = new A();
- }
+ public static void main(String[] args) {
+ A a1 = new A();
+ A a2 = new A();
+ }
}
```
@@ -1271,198 +1221,201 @@ public class A {
123
```
-**4. 静态内部类**
+**4. 静态内部类**
非静态内部类依赖于外部类的实例,而静态内部类不需要。
```java
-public class OuterClass {
+public class OuterClass {
- class InnerClass {
- }
+ class InnerClass {
+ }
- static class StaticInnerClass {
- }
+ static class StaticInnerClass {
+ }
- public static void main(String[] args) {
- // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
- OuterClass outerClass = new OuterClass();
- InnerClass innerClass = outerClass.new InnerClass();
- StaticInnerClass staticInnerClass = new StaticInnerClass();
- }
+ public static void main(String[] args) {
+ // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
+ OuterClass outerClass = new OuterClass();
+ InnerClass innerClass = outerClass.new InnerClass();
+ StaticInnerClass staticInnerClass = new StaticInnerClass();
+ }
}
```
静态内部类不能访问外部类的非静态的变量和方法。
-**5. 静态导包**
+**5. 静态导包**
-在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
+在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
```java
-import static com.xxx.ClassName.*
+import static com.xxx.ClassName.*
```
-**6. 初始化顺序**
+**6. 初始化顺序**
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
```java
-public static String staticField = "静态变量";
+public static String staticField = "静态变量";
```
```java
-static {
- System.out.println("静态语句块");
+static {
+ System.out.println("静态语句块");
}
```
```java
-public String field = "实例变量";
+public String field = "实例变量";
```
```java
{
- System.out.println("普通语句块");
+ System.out.println("普通语句块");
}
```
最后才是构造函数的初始化。
```java
-public InitialOrderTest() {
- System.out.println("构造函数");
+public InitialOrderTest() {
+ System.out.println("构造函数");
}
```
存在继承的情况下,初始化顺序为:
-- 父类(静态变量、静态语句块)
-- 子类(静态变量、静态语句块)
-- 父类(实例变量、普通语句块)
-- 父类(构造函数)
-- 子类(实例变量、普通语句块)
-- 子类(构造函数)
+- 父类(静态变量、静态语句块)
+- 子类(静态变量、静态语句块)
+- 父类(实例变量、普通语句块)
+- 父类(构造函数)
+- 子类(实例变量、普通语句块)
+- 子类(构造函数)
-# 七、反射
+# 七、反射
-每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
+每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
-类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。
+类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。
-反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
+反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
-Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
+Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
-- **Field**:可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
-- **Method**:可以使用 invoke() 方法调用与 Method 对象关联的方法;
-- **Constructor**:可以用 Constructor 创建新的对象。
+- **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
+- **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法;
+- **Constructor** :可以用 Constructor 创建新的对象。
-**反射的优点:**
+**反射的优点:**
-* **可扩展性** :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
-* **类浏览器和可视化开发环境** :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
-* **调试器和测试工具** : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
+* **可扩展性** :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
+* **类浏览器和可视化开发环境** :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
+* **调试器和测试工具** : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
-**反射的缺点:**
+**反射的缺点:**
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
-* **性能开销** :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
+* **性能开销** :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
-* **安全限制** :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
+* **安全限制** :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
-* **内部暴露** :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
+* **内部暴露** :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
-- [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html)
-- [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/)
+- [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html)
+- [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/)
-# 八、异常
+# 八、异常
-Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:**Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
+Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
-- **受检异常**:需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
-- **非受检异常**:是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。
+- **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
+- **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。
-
+
-- [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception)
-- [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html)
+- [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception)
+- [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html)
-# 九、泛型
+# 九、泛型
```java
-public class Box {
- // T stands for "Type"
- private T t;
- public void set(T t) { this.t = t; }
- public T get() { return t; }
+public class Box {
+ // T stands for "Type"
+ private T t;
+ public void set(T t) { this.t = t; }
+ public T get() { return t; }
}
```
-- [Java 泛型详解](http://www.importnew.com/24029.html)
-- [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
+- [Java 泛型详解](http://www.importnew.com/24029.html)
+- [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
-# 十、注解
+# 十、注解
-Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
+Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
-[注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html)
+[注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html)
-# 十一、特性
+# 十一、特性
-## Java 各版本的新特性
+## Java 各版本的新特性
-**New highlights in Java SE 8**
+**New highlights in Java SE 8**
-1. Lambda Expressions
-2. Pipelines and Streams
-3. Date and Time API
-4. Default Methods
-5. Type Annotations
-6. Nashhorn JavaScript Engine
-7. Concurrent Accumulators
-8. Parallel operations
-9. PermGen Error Removed
+1. Lambda Expressions
+2. Pipelines and Streams
+3. Date and Time API
+4. Default Methods
+5. Type Annotations
+6. Nashhorn JavaScript Engine
+7. Concurrent Accumulators
+8. Parallel operations
+9. PermGen Error Removed
-**New highlights in Java SE 7**
+**New highlights in Java SE 7**
-1. Strings in Switch Statement
-2. Type Inference for Generic Instance Creation
-3. Multiple Exception Handling
-4. Support for Dynamic Languages
-5. Try with Resources
-6. Java nio Package
-7. Binary Literals, Underscore in literals
-8. Diamond Syntax
+1. Strings in Switch Statement
+2. Type Inference for Generic Instance Creation
+3. Multiple Exception Handling
+4. Support for Dynamic Languages
+5. Try with Resources
+6. Java nio Package
+7. Binary Literals, Underscore in literals
+8. Diamond Syntax
-- [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17)
-- [Java 8 特性](http://www.importnew.com/19345.html)
+- [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17)
+- [Java 8 特性](http://www.importnew.com/19345.html)
-## Java 与 C++ 的区别
+## Java 与 C++ 的区别
-- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
-- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
-- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
-- Java 支持自动垃圾回收,而 C++ 需要手动回收。
-- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
-- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
-- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
-- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。
+- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
+- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
+- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
+- Java 支持自动垃圾回收,而 C++ 需要手动回收。
+- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
+- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
+- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
+- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。
-[What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php)
+[What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php)
-## JRE or JDK
+## JRE or JDK
-- JRE is the JVM program, Java application need to run on JRE.
-- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac"
+- JRE is the JVM program, Java application need to run on JRE.
+- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac"
-# 参考资料
+# 参考资料
-- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002.
-- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017.
----bottom---CyC---
-
-
+- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002.
+- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017.
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Java 容器.md b/docs/notes/Java 容器.md
index 752a85c1..ff906df2 100644
--- a/docs/notes/Java 容器.md
+++ b/docs/notes/Java 容器.md
@@ -1,3 +1,23 @@
+
+* [一、概览](#一概览)
+ * [Collection](#collection)
+ * [Map](#map)
+* [二、容器中的设计模式](#二容器中的设计模式)
+ * [迭代器模式](#迭代器模式)
+ * [适配器模式](#适配器模式)
+* [三、源码分析](#三源码分析)
+ * [ArrayList](#arraylist)
+ * [Vector](#vector)
+ * [CopyOnWriteArrayList](#copyonwritearraylist)
+ * [LinkedList](#linkedlist)
+ * [HashMap](#hashmap)
+ * [ConcurrentHashMap](#concurrenthashmap)
+ * [LinkedHashMap](#linkedhashmap)
+ * [WeakHashMap](#weakhashmap)
+* [参考资料](#参考资料)
+
+
+
# 一、概览
@@ -5,7 +25,7 @@
## Collection
-
+
### 1. Set
@@ -32,7 +52,7 @@
## Map
-
+
- TreeMap:基于红黑树实现。
@@ -47,7 +67,7 @@
## 迭代器模式
-
+
Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
@@ -108,7 +128,7 @@ public class ArrayList extends AbstractList
private static final int DEFAULT_CAPACITY = 10;
```
-
+
### 2. 扩容
@@ -372,7 +392,7 @@ transient Node first;
transient Node last;
```
-
+
### 2. 与 ArrayList 的比较
@@ -394,7 +414,7 @@ transient Entry[] table;
Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值相同的 Entry。
-
+
```java
static class Entry implements Map.Entry {
@@ -470,7 +490,7 @@ map.put("K3", "V3");
- 计算键值对所在的桶;
- 在链表上顺序查找,时间复杂度显然和链表的长度成正比。
-
+
### 3. put 操作
@@ -560,7 +580,7 @@ int hash = hash(key);
int i = indexFor(hash, table.length);
```
-**4.1 计算 hash 值**
+**4.1 计算 hash 值**
```java
final int hash(Object k) {
@@ -585,7 +605,7 @@ public final int hashCode() {
}
```
-**4.2 取模**
+**4.2 取模**
令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:
@@ -806,7 +826,7 @@ final Segment[] segments;
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
```
-
+
### 2. size 操作
@@ -1092,12 +1112,9 @@ public final class ConcurrentCache {
- [Java 集合细节(二):asList 的缺陷](http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtysix.html)
- [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/)
----bottom---CyC---
-
-
-
-
-
-
-
-
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Java 并发.md b/docs/notes/Java 并发.md
index 48017402..b022c2e7 100644
--- a/docs/notes/Java 并发.md
+++ b/docs/notes/Java 并发.md
@@ -1,11 +1,4 @@
-<<<<<<< HEAD
-# 一、线程状态转换
-
-
-
-## 新建(New)
-=======
-
+
* [一、线程状态转换](#一线程状态转换)
* [新建(New)](#新建new)
* [可运行(Runnable)](#可运行runnable)
@@ -71,565 +64,560 @@
## 新建(New)
->>>>>>> 9f680db0cc99bd992c7f979442ecf458a33f9c1b
创建后尚未启动。
-## 可运行(Runnable)
+## 可运行(Runnable)
-可能正在运行,也可能正在等待 CPU 时间片。
+可能正在运行,也可能正在等待 CPU 时间片。
-包含了操作系统线程状态中的 Running 和 Ready。
+包含了操作系统线程状态中的 Running 和 Ready。
-## 阻塞(Blocking)
+## 阻塞(Blocking)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
-## 无限期等待(Waiting)
+## 无限期等待(Waiting)
-等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
+等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
-| 进入方法 | 退出方法 |
-| --- | --- |
-| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
-| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
-| LockSupport.park() 方法 | LockSupport.unpark(Thread) |
+| 进入方法 | 退出方法 |
+| --- | --- |
+| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
+| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
+| LockSupport.park() 方法 | LockSupport.unpark(Thread) |
-## 限期等待(Timed Waiting)
+## 限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
-调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
+调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
-调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
+调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
-阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
+阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
-| 进入方法 | 退出方法 |
-| --- | --- |
-| Thread.sleep() 方法 | 时间结束 |
-| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
-| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
-| LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
-| LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
+| 进入方法 | 退出方法 |
+| --- | --- |
+| Thread.sleep() 方法 | 时间结束 |
+| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
+| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
+| LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
+| LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
-## 死亡(Terminated)
+## 死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
-# 二、使用线程
+# 二、使用线程
有三种使用线程的方法:
-- 实现 Runnable 接口;
-- 实现 Callable 接口;
-- 继承 Thread 类。
+- 实现 Runnable 接口;
+- 实现 Callable 接口;
+- 继承 Thread 类。
-实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
+实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
-## 实现 Runnable 接口
+## 实现 Runnable 接口
-需要实现 run() 方法。
+需要实现 run() 方法。
-通过 Thread 调用 start() 方法来启动线程。
+通过 Thread 调用 start() 方法来启动线程。
```java
-public class MyRunnable implements Runnable {
- public void run() {
- // ...
- }
+public class MyRunnable implements Runnable {
+ public void run() {
+ // ...
+ }
}
```
```java
-public static void main(String[] args) {
- MyRunnable instance = new MyRunnable();
- Thread thread = new Thread(instance);
- thread.start();
+public static void main(String[] args) {
+ MyRunnable instance = new MyRunnable();
+ Thread thread = new Thread(instance);
+ thread.start();
}
```
-## 实现 Callable 接口
+## 实现 Callable 接口
-与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
+与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
```java
-public class MyCallable implements Callable {
- public Integer call() {
- return 123;
- }
+public class MyCallable implements Callable {
+ public Integer call() {
+ return 123;
+ }
}
```
```java
-public static void main(String[] args) throws ExecutionException, InterruptedException {
- MyCallable mc = new MyCallable();
- FutureTask ft = new FutureTask<>(mc);
- Thread thread = new Thread(ft);
- thread.start();
- System.out.println(ft.get());
+public static void main(String[] args) throws ExecutionException, InterruptedException {
+ MyCallable mc = new MyCallable();
+ FutureTask ft = new FutureTask<>(mc);
+ Thread thread = new Thread(ft);
+ thread.start();
+ System.out.println(ft.get());
}
```
-## 继承 Thread 类
+## 继承 Thread 类
-同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
+同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
-当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
+当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
```java
-public class MyThread extends Thread {
- public void run() {
- // ...
- }
+public class MyThread extends Thread {
+ public void run() {
+ // ...
+ }
}
```
```java
-public static void main(String[] args) {
- MyThread mt = new MyThread();
- mt.start();
+public static void main(String[] args) {
+ MyThread mt = new MyThread();
+ mt.start();
}
```
-## 实现接口 VS 继承 Thread
+## 实现接口 VS 继承 Thread
实现接口会更好一些,因为:
-- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
-- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
+- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
+- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
-# 三、基础线程机制
+# 三、基础线程机制
-## Executor
+## Executor
-Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
+Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
-主要有三种 Executor:
+主要有三种 Executor:
-- CachedThreadPool:一个任务创建一个线程;
-- FixedThreadPool:所有任务只能使用固定大小的线程;
-- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
+- CachedThreadPool:一个任务创建一个线程;
+- FixedThreadPool:所有任务只能使用固定大小的线程;
+- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
```java
-public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < 5; i++) {
- executorService.execute(new MyRunnable());
- }
- executorService.shutdown();
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < 5; i++) {
+ executorService.execute(new MyRunnable());
+ }
+ executorService.shutdown();
}
```
-## Daemon
+## Daemon
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
-main() 属于非守护线程。
+main() 属于非守护线程。
-使用 setDaemon() 方法将一个线程设置为守护线程。
+使用 setDaemon() 方法将一个线程设置为守护线程。
```java
-public static void main(String[] args) {
- Thread thread = new Thread(new MyRunnable());
- thread.setDaemon(true);
+public static void main(String[] args) {
+ Thread thread = new Thread(new MyRunnable());
+ thread.setDaemon(true);
}
```
-## sleep()
+## sleep()
-Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
+Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
-sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
+sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
```java
-public void run() {
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
+public void run() {
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
}
```
-## yield()
+## yield()
-对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
+对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
```java
-public void run() {
- Thread.yield();
+public void run() {
+ Thread.yield();
}
```
-# 四、中断
+# 四、中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
-## InterruptedException
+## InterruptedException
-<<<<<<< HEAD
-通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
-=======
-通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、有限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
->>>>>>> 9f680db0cc99bd992c7f979442ecf458a33f9c1b
+通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
-对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
+对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
```java
-public class InterruptExample {
+public class InterruptExample {
- private static class MyThread1 extends Thread {
- @Override
- public void run() {
- try {
- Thread.sleep(2000);
- System.out.println("Thread run");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
+ private static class MyThread1 extends Thread {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(2000);
+ System.out.println("Thread run");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- Thread thread1 = new MyThread1();
- thread1.start();
- thread1.interrupt();
- System.out.println("Main run");
+public static void main(String[] args) throws InterruptedException {
+ Thread thread1 = new MyThread1();
+ thread1.start();
+ thread1.interrupt();
+ System.out.println("Main run");
}
```
```html
-Main run
-java.lang.InterruptedException: sleep interrupted
- at java.lang.Thread.sleep(Native Method)
- at InterruptExample.lambda$main$0(InterruptExample.java:5)
- at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
- at java.lang.Thread.run(Thread.java:745)
+Main run
+java.lang.InterruptedException: sleep interrupted
+ at java.lang.Thread.sleep(Native Method)
+ at InterruptExample.lambda$main$0(InterruptExample.java:5)
+ at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
+ at java.lang.Thread.run(Thread.java:745)
```
-## interrupted()
+## interrupted()
-如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
+如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
-但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
+但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
```java
-public class InterruptExample {
+public class InterruptExample {
- private static class MyThread2 extends Thread {
- @Override
- public void run() {
- while (!interrupted()) {
- // ..
- }
- System.out.println("Thread end");
- }
- }
+ private static class MyThread2 extends Thread {
+ @Override
+ public void run() {
+ while (!interrupted()) {
+ // ..
+ }
+ System.out.println("Thread end");
+ }
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- Thread thread2 = new MyThread2();
- thread2.start();
- thread2.interrupt();
+public static void main(String[] args) throws InterruptedException {
+ Thread thread2 = new MyThread2();
+ thread2.start();
+ thread2.interrupt();
}
```
```html
-Thread end
+Thread end
```
-## Executor 的中断操作
+## Executor 的中断操作
-调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
+调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
-以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
+以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
```java
-public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> {
- try {
- Thread.sleep(2000);
- System.out.println("Thread run");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- executorService.shutdownNow();
- System.out.println("Main run");
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> {
+ try {
+ Thread.sleep(2000);
+ System.out.println("Thread run");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ });
+ executorService.shutdownNow();
+ System.out.println("Main run");
}
```
```html
-Main run
-java.lang.InterruptedException: sleep interrupted
- at java.lang.Thread.sleep(Native Method)
- at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
- at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
- at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
- at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
- at java.lang.Thread.run(Thread.java:745)
+Main run
+java.lang.InterruptedException: sleep interrupted
+ at java.lang.Thread.sleep(Native Method)
+ at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
+ at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
+ at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
+ at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
+ at java.lang.Thread.run(Thread.java:745)
```
-如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
+如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
```java
-Future> future = executorService.submit(() -> {
- // ..
+Future> future = executorService.submit(() -> {
+ // ..
});
future.cancel(true);
```
-# 五、互斥同步
+# 五、互斥同步
-Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
+Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
-## synchronized
+## synchronized
-**1. 同步一个代码块**
+**1. 同步一个代码块**
```java
-public void func() {
- synchronized (this) {
- // ...
- }
+public void func() {
+ synchronized (this) {
+ // ...
+ }
}
```
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
-对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
+对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
```java
-public class SynchronizedExample {
+public class SynchronizedExample {
- public void func1() {
- synchronized (this) {
- for (int i = 0; i < 10; i++) {
- System.out.print(i + " ");
- }
- }
- }
+ public void func1() {
+ synchronized (this) {
+ for (int i = 0; i < 10; i++) {
+ System.out.print(i + " ");
+ }
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- SynchronizedExample e1 = new SynchronizedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> e1.func1());
- executorService.execute(() -> e1.func1());
+public static void main(String[] args) {
+ SynchronizedExample e1 = new SynchronizedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> e1.func1());
+ executorService.execute(() -> e1.func1());
}
```
```html
-0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
+0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
```java
-public static void main(String[] args) {
- SynchronizedExample e1 = new SynchronizedExample();
- SynchronizedExample e2 = new SynchronizedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> e1.func1());
- executorService.execute(() -> e2.func1());
+public static void main(String[] args) {
+ SynchronizedExample e1 = new SynchronizedExample();
+ SynchronizedExample e2 = new SynchronizedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> e1.func1());
+ executorService.execute(() -> e2.func1());
}
```
```html
-0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
+0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
```
-**2. 同步一个方法**
+**2. 同步一个方法**
```java
-public synchronized void func () {
- // ...
+public synchronized void func () {
+ // ...
}
```
它和同步代码块一样,作用于同一个对象。
-**3. 同步一个类**
+**3. 同步一个类**
```java
-public void func() {
- synchronized (SynchronizedExample.class) {
- // ...
- }
+public void func() {
+ synchronized (SynchronizedExample.class) {
+ // ...
+ }
}
```
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
```java
-public class SynchronizedExample {
+public class SynchronizedExample {
- public void func2() {
- synchronized (SynchronizedExample.class) {
- for (int i = 0; i < 10; i++) {
- System.out.print(i + " ");
- }
- }
- }
+ public void func2() {
+ synchronized (SynchronizedExample.class) {
+ for (int i = 0; i < 10; i++) {
+ System.out.print(i + " ");
+ }
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- SynchronizedExample e1 = new SynchronizedExample();
- SynchronizedExample e2 = new SynchronizedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> e1.func2());
- executorService.execute(() -> e2.func2());
+public static void main(String[] args) {
+ SynchronizedExample e1 = new SynchronizedExample();
+ SynchronizedExample e2 = new SynchronizedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> e1.func2());
+ executorService.execute(() -> e2.func2());
}
```
```html
-0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
+0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
-**4. 同步一个静态方法**
+**4. 同步一个静态方法**
```java
-public synchronized static void fun() {
- // ...
+public synchronized static void fun() {
+ // ...
}
```
作用于整个类。
-## ReentrantLock
+## ReentrantLock
-ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
+ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
```java
-public class LockExample {
+public class LockExample {
- private Lock lock = new ReentrantLock();
+ private Lock lock = new ReentrantLock();
- public void func() {
- lock.lock();
- try {
- for (int i = 0; i < 10; i++) {
- System.out.print(i + " ");
- }
- } finally {
- lock.unlock(); // 确保释放锁,从而避免发生死锁。
- }
- }
+ public void func() {
+ lock.lock();
+ try {
+ for (int i = 0; i < 10; i++) {
+ System.out.print(i + " ");
+ }
+ } finally {
+ lock.unlock(); // 确保释放锁,从而避免发生死锁。
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- LockExample lockExample = new LockExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> lockExample.func());
- executorService.execute(() -> lockExample.func());
+public static void main(String[] args) {
+ LockExample lockExample = new LockExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> lockExample.func());
+ executorService.execute(() -> lockExample.func());
}
```
```html
-0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
+0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
-## 比较
+## 比较
-**1. 锁的实现**
+**1. 锁的实现**
-synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
+synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
-**2. 性能**
+**2. 性能**
-新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
+新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
-**3. 等待可中断**
+**3. 等待可中断**
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
-ReentrantLock 可中断,而 synchronized 不行。
+ReentrantLock 可中断,而 synchronized 不行。
-**4. 公平锁**
+**4. 公平锁**
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
-synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
+synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
-**5. 锁绑定多个条件**
+**5. 锁绑定多个条件**
-一个 ReentrantLock 可以同时绑定多个 Condition 对象。
+一个 ReentrantLock 可以同时绑定多个 Condition 对象。
-## 使用选择
+## 使用选择
-除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
+除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
-# 六、线程之间的协作
+# 六、线程之间的协作
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。
-## join()
+## join()
-在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
+在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
-对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
+对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
```java
-public class JoinExample {
+public class JoinExample {
- private class A extends Thread {
- @Override
- public void run() {
- System.out.println("A");
- }
- }
+ private class A extends Thread {
+ @Override
+ public void run() {
+ System.out.println("A");
+ }
+ }
- private class B extends Thread {
+ private class B extends Thread {
- private A a;
+ private A a;
- B(A a) {
- this.a = a;
- }
+ B(A a) {
+ this.a = a;
+ }
- @Override
- public void run() {
- try {
- a.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("B");
- }
- }
+ @Override
+ public void run() {
+ try {
+ a.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println("B");
+ }
+ }
- public void test() {
- A a = new A();
- B b = new B(a);
- b.start();
- a.start();
- }
+ public void test() {
+ A a = new A();
+ B b = new B(a);
+ b.start();
+ a.start();
+ }
}
```
```java
-public static void main(String[] args) {
- JoinExample example = new JoinExample();
- example.test();
+public static void main(String[] args) {
+ JoinExample example = new JoinExample();
+ example.test();
}
```
@@ -638,41 +626,41 @@ A
B
```
-## wait() notify() notifyAll()
+## wait() notify() notifyAll()
-调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
+调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
-它们都属于 Object 的一部分,而不属于 Thread。
+它们都属于 Object 的一部分,而不属于 Thread。
-只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。
+只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。
-使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
+使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
```java
-public class WaitNotifyExample {
+public class WaitNotifyExample {
- public synchronized void before() {
- System.out.println("before");
- notifyAll();
- }
+ public synchronized void before() {
+ System.out.println("before");
+ notifyAll();
+ }
- public synchronized void after() {
- try {
- wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("after");
- }
+ public synchronized void after() {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println("after");
+ }
}
```
```java
-public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- WaitNotifyExample example = new WaitNotifyExample();
- executorService.execute(() -> example.after());
- executorService.execute(() -> example.before());
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ WaitNotifyExample example = new WaitNotifyExample();
+ executorService.execute(() -> example.after());
+ executorService.execute(() -> example.before());
}
```
@@ -681,55 +669,55 @@ before
after
```
-**wait() 和 sleep() 的区别**
+**wait() 和 sleep() 的区别**
-- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
-- wait() 会释放锁,sleep() 不会。
+- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
+- wait() 会释放锁,sleep() 不会。
-## await() signal() signalAll()
+## await() signal() signalAll()
-java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
+java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
-相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
+相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
-使用 Lock 来获取一个 Condition 对象。
+使用 Lock 来获取一个 Condition 对象。
```java
-public class AwaitSignalExample {
+public class AwaitSignalExample {
- private Lock lock = new ReentrantLock();
- private Condition condition = lock.newCondition();
+ private Lock lock = new ReentrantLock();
+ private Condition condition = lock.newCondition();
- public void before() {
- lock.lock();
- try {
- System.out.println("before");
- condition.signalAll();
- } finally {
- lock.unlock();
- }
- }
+ public void before() {
+ lock.lock();
+ try {
+ System.out.println("before");
+ condition.signalAll();
+ } finally {
+ lock.unlock();
+ }
+ }
- public void after() {
- lock.lock();
- try {
- condition.await();
- System.out.println("after");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- }
+ public void after() {
+ lock.lock();
+ try {
+ condition.await();
+ System.out.println("after");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ lock.unlock();
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- AwaitSignalExample example = new AwaitSignalExample();
- executorService.execute(() -> example.after());
- executorService.execute(() -> example.before());
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ AwaitSignalExample example = new AwaitSignalExample();
+ executorService.execute(() -> example.after());
+ executorService.execute(() -> example.before());
}
```
@@ -738,35 +726,35 @@ before
after
```
-# 七、J.U.C - AQS
+# 七、J.U.C - AQS
-java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
+java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
-## CountDownLatch
+## CountDownLatch
用来控制一个线程等待多个线程。
-维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
+维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
-
+
```java
-public class CountdownLatchExample {
+public class CountdownLatchExample {
- public static void main(String[] args) throws InterruptedException {
- final int totalThread = 10;
- CountDownLatch countDownLatch = new CountDownLatch(totalThread);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < totalThread; i++) {
- executorService.execute(() -> {
- System.out.print("run..");
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- System.out.println("end");
- executorService.shutdown();
- }
+ public static void main(String[] args) throws InterruptedException {
+ final int totalThread = 10;
+ CountDownLatch countDownLatch = new CountDownLatch(totalThread);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < totalThread; i++) {
+ executorService.execute(() -> {
+ System.out.print("run..");
+ countDownLatch.countDown();
+ });
+ }
+ countDownLatch.await();
+ System.out.println("end");
+ executorService.shutdown();
+ }
}
```
@@ -774,51 +762,51 @@ public class CountdownLatchExample {
run..run..run..run..run..run..run..run..run..run..end
```
-## CyclicBarrier
+## CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
-和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
+和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
-CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
+CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
-CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
+CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
```java
-public CyclicBarrier(int parties, Runnable barrierAction) {
- if (parties <= 0) throw new IllegalArgumentException();
- this.parties = parties;
- this.count = parties;
- this.barrierCommand = barrierAction;
+public CyclicBarrier(int parties, Runnable barrierAction) {
+ if (parties <= 0) throw new IllegalArgumentException();
+ this.parties = parties;
+ this.count = parties;
+ this.barrierCommand = barrierAction;
}
-public CyclicBarrier(int parties) {
- this(parties, null);
+public CyclicBarrier(int parties) {
+ this(parties, null);
}
```
-
+
```java
-public class CyclicBarrierExample {
+public class CyclicBarrierExample {
- public static void main(String[] args) {
- final int totalThread = 10;
- CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < totalThread; i++) {
- executorService.execute(() -> {
- System.out.print("before..");
- try {
- cyclicBarrier.await();
- } catch (InterruptedException | BrokenBarrierException e) {
- e.printStackTrace();
- }
- System.out.print("after..");
- });
- }
- executorService.shutdown();
- }
+ public static void main(String[] args) {
+ final int totalThread = 10;
+ CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < totalThread; i++) {
+ executorService.execute(() -> {
+ System.out.print("before..");
+ try {
+ cyclicBarrier.await();
+ } catch (InterruptedException | BrokenBarrierException e) {
+ e.printStackTrace();
+ }
+ System.out.print("after..");
+ });
+ }
+ executorService.shutdown();
+ }
}
```
@@ -826,152 +814,152 @@ public class CyclicBarrierExample {
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
```
-## Semaphore
+## Semaphore
-Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
+Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
-以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
+以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
```java
-public class SemaphoreExample {
+public class SemaphoreExample {
- public static void main(String[] args) {
- final int clientCount = 3;
- final int totalRequestCount = 10;
- Semaphore semaphore = new Semaphore(clientCount);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < totalRequestCount; i++) {
- executorService.execute(()->{
- try {
- semaphore.acquire();
- System.out.print(semaphore.availablePermits() + " ");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- semaphore.release();
- }
- });
- }
- executorService.shutdown();
- }
+ public static void main(String[] args) {
+ final int clientCount = 3;
+ final int totalRequestCount = 10;
+ Semaphore semaphore = new Semaphore(clientCount);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < totalRequestCount; i++) {
+ executorService.execute(()->{
+ try {
+ semaphore.acquire();
+ System.out.print(semaphore.availablePermits() + " ");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ semaphore.release();
+ }
+ });
+ }
+ executorService.shutdown();
+ }
}
```
```html
-2 1 2 2 2 2 2 1 2 2
+2 1 2 2 2 2 2 1 2 2
```
-# 八、J.U.C - 其它组件
+# 八、J.U.C - 其它组件
-## FutureTask
+## FutureTask
-在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
+在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
```java
-public class FutureTask implements RunnableFuture
+public class FutureTask implements RunnableFuture
```
```java
-public interface RunnableFuture extends Runnable, Future
+public interface RunnableFuture extends Runnable, Future
```
-FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。
+FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。
```java
-public class FutureTaskExample {
+public class FutureTaskExample {
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- FutureTask futureTask = new FutureTask(new Callable() {
- @Override
- public Integer call() throws Exception {
- int result = 0;
- for (int i = 0; i < 100; i++) {
- Thread.sleep(10);
- result += i;
- }
- return result;
- }
- });
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+ FutureTask futureTask = new FutureTask(new Callable() {
+ @Override
+ public Integer call() throws Exception {
+ int result = 0;
+ for (int i = 0; i < 100; i++) {
+ Thread.sleep(10);
+ result += i;
+ }
+ return result;
+ }
+ });
- Thread computeThread = new Thread(futureTask);
- computeThread.start();
+ Thread computeThread = new Thread(futureTask);
+ computeThread.start();
- Thread otherThread = new Thread(() -> {
- System.out.println("other task is running...");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- otherThread.start();
- System.out.println(futureTask.get());
- }
+ Thread otherThread = new Thread(() -> {
+ System.out.println("other task is running...");
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ });
+ otherThread.start();
+ System.out.println(futureTask.get());
+ }
}
```
```html
-other task is running...
+other task is running...
4950
```
-## BlockingQueue
+## BlockingQueue
-java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
+java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
-- **FIFO 队列**:LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
-- **优先级队列**:PriorityBlockingQueue
+- **FIFO 队列** :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
+- **优先级队列** :PriorityBlockingQueue
-提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。
+提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。
-**使用 BlockingQueue 实现生产者消费者问题**
+**使用 BlockingQueue 实现生产者消费者问题**
```java
-public class ProducerConsumer {
+public class ProducerConsumer {
- private static BlockingQueue queue = new ArrayBlockingQueue<>(5);
+ private static BlockingQueue queue = new ArrayBlockingQueue<>(5);
- private static class Producer extends Thread {
- @Override
- public void run() {
- try {
- queue.put("product");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.print("produce..");
- }
- }
+ private static class Producer extends Thread {
+ @Override
+ public void run() {
+ try {
+ queue.put("product");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.print("produce..");
+ }
+ }
- private static class Consumer extends Thread {
+ private static class Consumer extends Thread {
- @Override
- public void run() {
- try {
- String product = queue.take();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.print("consume..");
- }
- }
+ @Override
+ public void run() {
+ try {
+ String product = queue.take();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.print("consume..");
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- for (int i = 0; i < 2; i++) {
- Producer producer = new Producer();
- producer.start();
- }
- for (int i = 0; i < 5; i++) {
- Consumer consumer = new Consumer();
- consumer.start();
- }
- for (int i = 0; i < 3; i++) {
- Producer producer = new Producer();
- producer.start();
- }
+public static void main(String[] args) {
+ for (int i = 0; i < 2; i++) {
+ Producer producer = new Producer();
+ producer.start();
+ }
+ for (int i = 0; i < 5; i++) {
+ Consumer consumer = new Consumer();
+ consumer.start();
+ }
+ for (int i = 0; i < 3; i++) {
+ Producer producer = new Producer();
+ producer.start();
+ }
}
```
@@ -979,99 +967,99 @@ public static void main(String[] args) {
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
```
-## ForkJoin
+## ForkJoin
-主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
+主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
```java
-public class ForkJoinExample extends RecursiveTask {
+public class ForkJoinExample extends RecursiveTask {
- private final int threshold = 5;
- private int first;
- private int last;
+ private final int threshold = 5;
+ private int first;
+ private int last;
- public ForkJoinExample(int first, int last) {
- this.first = first;
- this.last = last;
- }
+ public ForkJoinExample(int first, int last) {
+ this.first = first;
+ this.last = last;
+ }
- @Override
- protected Integer compute() {
- int result = 0;
- if (last - first <= threshold) {
- // 任务足够小则直接计算
- for (int i = first; i <= last; i++) {
- result += i;
- }
- } else {
- // 拆分成小任务
- int middle = first + (last - first) / 2;
- ForkJoinExample leftTask = new ForkJoinExample(first, middle);
- ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
- leftTask.fork();
- rightTask.fork();
- result = leftTask.join() + rightTask.join();
- }
- return result;
- }
+ @Override
+ protected Integer compute() {
+ int result = 0;
+ if (last - first <= threshold) {
+ // 任务足够小则直接计算
+ for (int i = first; i <= last; i++) {
+ result += i;
+ }
+ } else {
+ // 拆分成小任务
+ int middle = first + (last - first) / 2;
+ ForkJoinExample leftTask = new ForkJoinExample(first, middle);
+ ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
+ leftTask.fork();
+ rightTask.fork();
+ result = leftTask.join() + rightTask.join();
+ }
+ return result;
+ }
}
```
```java
-public static void main(String[] args) throws ExecutionException, InterruptedException {
- ForkJoinExample example = new ForkJoinExample(1, 10000);
- ForkJoinPool forkJoinPool = new ForkJoinPool();
- Future result = forkJoinPool.submit(example);
- System.out.println(result.get());
+public static void main(String[] args) throws ExecutionException, InterruptedException {
+ ForkJoinExample example = new ForkJoinExample(1, 10000);
+ ForkJoinPool forkJoinPool = new ForkJoinPool();
+ Future result = forkJoinPool.submit(example);
+ System.out.println(result.get());
}
```
-ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
+ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
```java
-public class ForkJoinPool extends AbstractExecutorService
+public class ForkJoinPool extends AbstractExecutorService
```
-ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。
+ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。
-
+
-# 九、线程不安全示例
+# 九、线程不安全示例
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
-以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。
+以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。
```java
-public class ThreadUnsafeExample {
+public class ThreadUnsafeExample {
- private int cnt = 0;
+ private int cnt = 0;
- public void add() {
- cnt++;
- }
+ public void add() {
+ cnt++;
+ }
- public int get() {
- return cnt;
- }
+ public int get() {
+ return cnt;
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- ThreadUnsafeExample example = new ThreadUnsafeExample();
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ ThreadUnsafeExample example = new ThreadUnsafeExample();
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
+ }
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
```
@@ -1079,88 +1067,88 @@ public static void main(String[] args) throws InterruptedException {
997
```
-# 十、Java 内存模型
+# 十、Java 内存模型
-Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
+Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
-## 主内存与工作内存
+## 主内存与工作内存
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
-
+
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
-
+
-## 内存间交互操作
+## 内存间交互操作
-Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
+Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
-
+
-- read:把一个变量的值从主内存传输到工作内存中
-- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
-- use:把工作内存中一个变量的值传递给执行引擎
-- assign:把一个从执行引擎接收到的值赋给工作内存的变量
-- store:把工作内存的一个变量的值传送到主内存中
-- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
-- lock:作用于主内存的变量
-- unlock
+- read:把一个变量的值从主内存传输到工作内存中
+- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
+- use:把工作内存中一个变量的值传递给执行引擎
+- assign:把一个从执行引擎接收到的值赋给工作内存的变量
+- store:把工作内存的一个变量的值传送到主内存中
+- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
+- lock:作用于主内存的变量
+- unlock
-## 内存模型三大特性
+## 内存模型三大特性
-### 1. 原子性
+### 1. 原子性
-Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
+Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
-有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
+有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
-为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。
+为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。
-下图演示了两个线程同时对 cnt 进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入旧值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
+下图演示了两个线程同时对 cnt 进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入旧值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
-
+
-AtomicInteger 能保证多个线程修改的原子性。
+AtomicInteger 能保证多个线程修改的原子性。
-
+
-使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:
+使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:
```java
-public class AtomicExample {
- private AtomicInteger cnt = new AtomicInteger();
+public class AtomicExample {
+ private AtomicInteger cnt = new AtomicInteger();
- public void add() {
- cnt.incrementAndGet();
- }
+ public void add() {
+ cnt.incrementAndGet();
+ }
- public int get() {
- return cnt.get();
- }
+ public int get() {
+ return cnt.get();
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- AtomicExample example = new AtomicExample(); // 只修改这条语句
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ AtomicExample example = new AtomicExample(); // 只修改这条语句
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
+ }
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
```
@@ -1168,37 +1156,37 @@ public static void main(String[] args) throws InterruptedException {
1000
```
-除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
+除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
```java
-public class AtomicSynchronizedExample {
- private int cnt = 0;
+public class AtomicSynchronizedExample {
+ private int cnt = 0;
- public synchronized void add() {
- cnt++;
- }
+ public synchronized void add() {
+ cnt++;
+ }
- public synchronized int get() {
- return cnt;
- }
+ public synchronized int get() {
+ return cnt;
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- AtomicSynchronizedExample example = new AtomicSynchronizedExample();
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ AtomicSynchronizedExample example = new AtomicSynchronizedExample();
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
+ }
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
```
@@ -1206,217 +1194,217 @@ public static void main(String[] args) throws InterruptedException {
1000
```
-### 2. 可见性
+### 2. 可见性
-可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
+可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
主要有三种实现可见性的方式:
-- volatile
-- synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
-- final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
+- volatile
+- synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
+- final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
-对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
+对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
-### 3. 有序性
+### 3. 有序性
-有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
+有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
-volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
+volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
-也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
+也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
-## 先行发生原则
+## 先行发生原则
-上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
+上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
-### 1. 单一线程原则
+### 1. 单一线程原则
-> Single Thread rule
+> Single Thread rule
在一个线程内,在程序前面的操作先行发生于后面的操作。
-
+
-### 2. 管程锁定规则
+### 2. 管程锁定规则
-> Monitor Lock Rule
+> Monitor Lock Rule
-一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
+一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
-
+
-### 3. volatile 变量规则
+### 3. volatile 变量规则
-> Volatile Variable Rule
+> Volatile Variable Rule
-对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
+对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
-
+
-### 4. 线程启动规则
+### 4. 线程启动规则
-> Thread Start Rule
+> Thread Start Rule
-Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
+Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
-
+
-### 5. 线程加入规则
+### 5. 线程加入规则
-> Thread Join Rule
+> Thread Join Rule
-Thread 对象的结束先行发生于 join() 方法返回。
+Thread 对象的结束先行发生于 join() 方法返回。
-
+
-### 6. 线程中断规则
+### 6. 线程中断规则
-> Thread Interruption Rule
+> Thread Interruption Rule
-对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
+对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
-### 7. 对象终结规则
+### 7. 对象终结规则
-> Finalizer Rule
+> Finalizer Rule
-一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
+一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
-### 8. 传递性
+### 8. 传递性
-> Transitivity
+> Transitivity
-如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
+如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
-# 十一、线程安全
+# 十一、线程安全
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
线程安全有以下几种实现方式:
-## 不可变
+## 不可变
不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
不可变的类型:
-- final 关键字修饰的基本数据类型
-- String
-- 枚举类型
-- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
+- final 关键字修饰的基本数据类型
+- String
+- 枚举类型
+- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
-对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
+对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
```java
-public class ImmutableExample {
- public static void main(String[] args) {
- Map map = new HashMap<>();
- Map unmodifiableMap = Collections.unmodifiableMap(map);
- unmodifiableMap.put("a", 1);
- }
+public class ImmutableExample {
+ public static void main(String[] args) {
+ Map map = new HashMap<>();
+ Map unmodifiableMap = Collections.unmodifiableMap(map);
+ unmodifiableMap.put("a", 1);
+ }
}
```
```html
-Exception in thread "main" java.lang.UnsupportedOperationException
- at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
- at ImmutableExample.main(ImmutableExample.java:9)
+Exception in thread "main" java.lang.UnsupportedOperationException
+ at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
+ at ImmutableExample.main(ImmutableExample.java:9)
```
-Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
+Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
```java
-public V put(K key, V value) {
- throw new UnsupportedOperationException();
+public V put(K key, V value) {
+ throw new UnsupportedOperationException();
}
```
-## 互斥同步
+## 互斥同步
-synchronized 和 ReentrantLock。
+synchronized 和 ReentrantLock。
-## 非阻塞同步
+## 非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
-### 1. CAS
+### 1. CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
-乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
+乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
-### 2. AtomicInteger
+### 2. AtomicInteger
-J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
+J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
-以下代码使用了 AtomicInteger 执行了自增的操作。
+以下代码使用了 AtomicInteger 执行了自增的操作。
```java
-private AtomicInteger cnt = new AtomicInteger();
+private AtomicInteger cnt = new AtomicInteger();
-public void add() {
- cnt.incrementAndGet();
+public void add() {
+ cnt.incrementAndGet();
}
```
-以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。
+以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。
```java
-public final int incrementAndGet() {
- return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
+public final int incrementAndGet() {
+ return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
```
-以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
+以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
-可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
+可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
```java
-public final int getAndAddInt(Object var1, long var2, int var4) {
- int var5;
- do {
- var5 = this.getIntVolatile(var1, var2);
- } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+public final int getAndAddInt(Object var1, long var2, int var4) {
+ int var5;
+ do {
+ var5 = this.getIntVolatile(var1, var2);
+ } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
- return var5;
+ return var5;
}
```
-### 3. ABA
+### 3. ABA
-如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
+如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
-J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
+J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
-## 无同步方案
+## 无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
-### 1. 栈封闭
+### 1. 栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
```java
-public class StackClosedExample {
- public void add100() {
- int cnt = 0;
- for (int i = 0; i < 100; i++) {
- cnt++;
- }
- System.out.println(cnt);
- }
+public class StackClosedExample {
+ public void add100() {
+ int cnt = 0;
+ for (int i = 0; i < 100; i++) {
+ cnt++;
+ }
+ System.out.println(cnt);
+ }
}
```
```java
-public static void main(String[] args) {
- StackClosedExample example = new StackClosedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> example.add100());
- executorService.execute(() -> example.add100());
- executorService.shutdown();
+public static void main(String[] args) {
+ StackClosedExample example = new StackClosedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> example.add100());
+ executorService.execute(() -> example.add100());
+ executorService.shutdown();
}
```
@@ -1425,37 +1413,37 @@ public static void main(String[] args) {
100
```
-### 2. 线程本地存储(Thread Local Storage)
+### 2. 线程本地存储(Thread Local Storage)
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
-符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
+符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
-可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
+可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
-对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。
+对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。
```java
-public class ThreadLocalExample {
- public static void main(String[] args) {
- ThreadLocal threadLocal = new ThreadLocal();
- Thread thread1 = new Thread(() -> {
- threadLocal.set(1);
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(threadLocal.get());
- threadLocal.remove();
- });
- Thread thread2 = new Thread(() -> {
- threadLocal.set(2);
- threadLocal.remove();
- });
- thread1.start();
- thread2.start();
- }
+public class ThreadLocalExample {
+ public static void main(String[] args) {
+ ThreadLocal threadLocal = new ThreadLocal();
+ Thread thread1 = new Thread(() -> {
+ threadLocal.set(1);
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println(threadLocal.get());
+ threadLocal.remove();
+ });
+ Thread thread2 = new Thread(() -> {
+ threadLocal.set(2);
+ threadLocal.remove();
+ });
+ thread1.start();
+ thread2.start();
+ }
}
```
@@ -1463,93 +1451,93 @@ public class ThreadLocalExample {
1
```
-为了理解 ThreadLocal,先看以下代码:
+为了理解 ThreadLocal,先看以下代码:
```java
-public class ThreadLocalExample1 {
- public static void main(String[] args) {
- ThreadLocal threadLocal1 = new ThreadLocal();
- ThreadLocal threadLocal2 = new ThreadLocal();
- Thread thread1 = new Thread(() -> {
- threadLocal1.set(1);
- threadLocal2.set(1);
- });
- Thread thread2 = new Thread(() -> {
- threadLocal1.set(2);
- threadLocal2.set(2);
- });
- thread1.start();
- thread2.start();
- }
+public class ThreadLocalExample1 {
+ public static void main(String[] args) {
+ ThreadLocal threadLocal1 = new ThreadLocal();
+ ThreadLocal threadLocal2 = new ThreadLocal();
+ Thread thread1 = new Thread(() -> {
+ threadLocal1.set(1);
+ threadLocal2.set(1);
+ });
+ Thread thread2 = new Thread(() -> {
+ threadLocal1.set(2);
+ threadLocal2.set(2);
+ });
+ thread1.start();
+ thread2.start();
+ }
}
```
它所对应的底层结构图为:
-
+
-每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
+每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
```java
-/* ThreadLocal values pertaining to this thread. This map is maintained
- * by the ThreadLocal class. */
-ThreadLocal.ThreadLocalMap threadLocals = null;
+/* ThreadLocal values pertaining to this thread. This map is maintained
+ * by the ThreadLocal class. */
+ThreadLocal.ThreadLocalMap threadLocals = null;
```
-当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。
+当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。
```java
-public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
+public void set(T value) {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null)
+ map.set(this, value);
+ else
+ createMap(t, value);
}
```
-get() 方法类似。
+get() 方法类似。
```java
-public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- return result;
- }
- }
- return setInitialValue();
+public T get() {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null) {
+ ThreadLocalMap.Entry e = map.getEntry(this);
+ if (e != null) {
+ @SuppressWarnings("unchecked")
+ T result = (T)e.value;
+ return result;
+ }
+ }
+ return setInitialValue();
}
```
-ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
+ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
-在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
+在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
-### 3. 可重入代码(Reentrant Code)
+### 3. 可重入代码(Reentrant Code)
-这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
+这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
-# 十二、锁优化
+# 十二、锁优化
-这里的锁优化主要是指 JVM 对 synchronized 的优化。
+这里的锁优化主要是指 JVM 对 synchronized 的优化。
-## 自旋锁
+## 自旋锁
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
-自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
+自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
-在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
+在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
-## 锁消除
+## 锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
@@ -1558,124 +1546,97 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
```java
-public static String concatString(String s1, String s2, String s3) {
- return s1 + s2 + s3;
+public static String concatString(String s1, String s2, String s3) {
+ return s1 + s2 + s3;
}
```
-String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:
+String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:
```java
-public static String concatString(String s1, String s2, String s3) {
- StringBuffer sb = new StringBuffer();
- sb.append(s1);
- sb.append(s2);
- sb.append(s3);
- return sb.toString();
+public static String concatString(String s1, String s2, String s3) {
+ StringBuffer sb = new StringBuffer();
+ sb.append(s1);
+ sb.append(s2);
+ sb.append(s3);
+ return sb.toString();
}
```
-每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
+每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
-## 锁粗化
+## 锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
-<<<<<<< HEAD
-上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
-=======
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
## 轻量级锁
-JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biased)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
+JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
->>>>>>> 9f680db0cc99bd992c7f979442ecf458a33f9c1b
-## 轻量级锁
+下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
-JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
+
-以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
+轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
-
+当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
-下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
+
-
+如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
-轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
+## 偏向锁
-当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
+偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
-
+当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
-如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
+当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
-## 偏向锁
+
-偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
+# 十三、多线程开发良好的实践
-当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
+- 给线程起个有意义的名字,这样可以方便找 Bug。
-当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
+- 缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。
-
+- 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。
-# 十三、多线程开发良好的实践
+- 使用 BlockingQueue 实现生产者消费者问题。
-- 给线程起个有意义的名字,这样可以方便找 Bug。
+- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
-- 缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。
+- 使用本地变量和不可变类来保证线程安全。
-- 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。
+- 使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。
-- 使用 BlockingQueue 实现生产者消费者问题。
+# 参考资料
-- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
+- BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007.
+- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
+- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html)
+- [线程通信](http://ifeve.com/thread-signaling/#missed_signal)
+- [Java 线程面试题 Top 50](http://www.importnew.com/12773.html)
+- [BlockingQueue](http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html)
+- [thread state java](https://stackoverflow.com/questions/11265289/thread-state-java)
+- [CSC 456 Spring 2012/ch7 MN](http://wiki.expertiza.ncsu.edu/index.php/CSC_456_Spring_2012/ch7_MN)
+- [Java - Understanding Happens-before relationship](https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html)
+- [6장 Thread Synchronization](https://www.slideshare.net/novathinker/6-thread-synchronization)
+- [How is Java's ThreadLocal implemented under the hood?](https://stackoverflow.com/questions/1202444/how-is-javas-threadlocal-implemented-under-the-hood/15653015)
+- [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1)
+- [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example")
+- [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/)
+- [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf)
-- 使用本地变量和不可变类来保证线程安全。
-- 使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。
-# 参考资料
-- BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007.
-- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
-- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html)
-- [线程通信](http://ifeve.com/thread-signaling/#missed_signal)
-- [Java 线程面试题 Top 50](http://www.importnew.com/12773.html)
-- [BlockingQueue](http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html)
-- [thread state java](https://stackoverflow.com/questions/11265289/thread-state-java)
-- [CSC 456 Spring 2012/ch7 MN](http://wiki.expertiza.ncsu.edu/index.php/CSC_456_Spring_2012/ch7_MN)
-- [Java - Understanding Happens-before relationship](https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html)
-- [6장 Thread Synchronization](https://www.slideshare.net/novathinker/6-thread-synchronization)
-- [How is Java's ThreadLocal implemented under the hood?](https://stackoverflow.com/questions/1202444/how-is-javas-threadlocal-implemented-under-the-hood/15653015)
-- [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1)
-- [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example")
-- [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/)
-- [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf)
----bottom---CyC---
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Java 虚拟机.md b/docs/notes/Java 虚拟机.md
index 27b62733..b8981a10 100644
--- a/docs/notes/Java 虚拟机.md
+++ b/docs/notes/Java 虚拟机.md
@@ -1,120 +1,150 @@
-# 一、运行时数据区域
+
+* [一、运行时数据区域](#一运行时数据区域)
+ * [程序计数器](#程序计数器)
+ * [Java 虚拟机栈](#java-虚拟机栈)
+ * [本地方法栈](#本地方法栈)
+ * [堆](#堆)
+ * [方法区](#方法区)
+ * [运行时常量池](#运行时常量池)
+ * [直接内存](#直接内存)
+* [二、垃圾收集](#二垃圾收集)
+ * [判断一个对象是否可被回收](#判断一个对象是否可被回收)
+ * [引用类型](#引用类型)
+ * [垃圾收集算法](#垃圾收集算法)
+ * [垃圾收集器](#垃圾收集器)
+* [三、内存分配与回收策略](#三内存分配与回收策略)
+ * [Minor GC 和 Full GC](#minor-gc-和-full-gc)
+ * [内存分配策略](#内存分配策略)
+ * [Full GC 的触发条件](#full-gc-的触发条件)
+* [四、类加载机制](#四类加载机制)
+ * [类的生命周期](#类的生命周期)
+ * [类加载过程](#类加载过程)
+ * [类初始化时机](#类初始化时机)
+ * [类与类加载器](#类与类加载器)
+ * [类加载器分类](#类加载器分类)
+ * [双亲委派模型](#双亲委派模型)
+ * [自定义类加载器实现](#自定义类加载器实现)
+* [参考资料](#参考资料)
+
-
-## 程序计数器
+# 一、运行时数据区域
+
+
+
+## 程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
-## Java 虚拟机栈
+## Java 虚拟机栈
-每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
+每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
-
+
-可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
+可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
```java
-java -Xss512M HackTheJava
+java -Xss512M HackTheJava
```
该区域可能抛出以下异常:
-- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
-- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
+- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
+- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
-## 本地方法栈
+## 本地方法栈
-本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
+本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
-本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
+本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
-
+
-## 堆
+## 堆
-所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
+所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
-- 新生代(Young Generation)
-- 老年代(Old Generation)
+- 新生代(Young Generation)
+- 老年代(Old Generation)
-堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
+堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
-可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
+可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
```java
-java -Xms1M -Xmx2M HackTheJava
+java -Xms1M -Xmx2M HackTheJava
```
-## 方法区
+## 方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
+和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
-HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
+HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
-## 运行时常量池
+## 运行时常量池
运行时常量池是方法区的一部分。
-Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
+Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
-除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
+除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
-## 直接内存
+## 直接内存
-在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
+在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
-# 二、垃圾收集
+# 二、垃圾收集
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
-## 判断一个对象是否可被回收
+## 判断一个对象是否可被回收
-### 1. 引用计数算法
+### 1. 引用计数算法
-为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
+为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
-在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
+在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
```java
-public class Test {
+public class Test {
- public Object instance = null;
+ public Object instance = null;
- public static void main(String[] args) {
- Test a = new Test();
- Test b = new Test();
- a.instance = b;
- b.instance = a;
- a = null;
- b = null;
- doSomething();
- }
+ public static void main(String[] args) {
+ Test a = new Test();
+ Test b = new Test();
+ a.instance = b;
+ b.instance = a;
+ a = null;
+ b = null;
+ doSomething();
+ }
}
```
-在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
+在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
-### 2. 可达性分析算法
+### 2. 可达性分析算法
-以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
+以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
-Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
+Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
-- 虚拟机栈中局部变量表中引用的对象
-- 本地方法栈中 JNI 中引用的对象
-- 方法区中类静态属性引用的对象
-- 方法区中的常量引用的对象
+- 虚拟机栈中局部变量表中引用的对象
+- 本地方法栈中 JNI 中引用的对象
+- 方法区中类静态属性引用的对象
+- 方法区中的常量引用的对象
-
+
-### 3. 方法区的回收
+### 3. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
@@ -124,632 +154,603 @@ Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
-- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
-- 加载该类的 ClassLoader 已经被回收。
-- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
+- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
+- 加载该类的 ClassLoader 已经被回收。
+- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
-### 4. finalize()
+### 4. finalize()
-类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
+类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
-当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
+当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
-## 引用类型
+## 引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
-Java 提供了四种强度不同的引用类型。
+Java 提供了四种强度不同的引用类型。
-### 1. 强引用
+### 1. 强引用
被强引用关联的对象不会被回收。
-使用 new 一个新对象的方式来创建强引用。
+使用 new 一个新对象的方式来创建强引用。
```java
-Object obj = new Object();
+Object obj = new Object();
```
-### 2. 软引用
+### 2. 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
-使用 SoftReference 类来创建软引用。
+使用 SoftReference 类来创建软引用。
```java
-Object obj = new Object();
-SoftReference sf = new SoftReference(obj);
-obj = null; // 使对象只被软引用关联
+Object obj = new Object();
+SoftReference sf = new SoftReference(obj);
+obj = null; // 使对象只被软引用关联
```
-### 3. 弱引用
+### 3. 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
-使用 WeakReference 类来创建弱引用。
+使用 WeakReference 类来创建弱引用。
```java
-Object obj = new Object();
-WeakReference wf = new WeakReference(obj);
-obj = null;
+Object obj = new Object();
+WeakReference wf = new WeakReference(obj);
+obj = null;
```
-### 4. 虚引用
+### 4. 虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
-使用 PhantomReference 来创建虚引用。
+使用 PhantomReference 来创建虚引用。
```java
-Object obj = new Object();
-PhantomReference pf = new PhantomReference(obj, null);
-obj = null;
+Object obj = new Object();
+PhantomReference pf = new PhantomReference(obj, null);
+obj = null;
```
-## 垃圾收集算法
+## 垃圾收集算法
-### 1. 标记 - 清除
+### 1. 标记 - 清除
-
+
标记要回收的对象,然后清除。
不足:
-- 标记和清除过程效率都不高;
-- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
+- 标记和清除过程效率都不高;
+- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
-### 2. 标记 - 整理
+### 2. 标记 - 整理
-
+
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:
-- 不会产生内存碎片
-
-不足:
-
-- 需要移动大量对象,处理效率比较低。
-
-<<<<<<< HEAD
-### 3. 复制
-=======
-优点:
-
- 不会产生内存碎片
不足:
-- 在标记-清除的基础上还需进行对象的移动,成本相对较高
+- 需要移动大量对象,处理效率比较低。
### 3. 复制
->>>>>>> 9f680db0cc99bd992c7f979442ecf458a33f9c1b
-
+
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
-现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
+现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
-HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
+HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
-### 4. 分代收集
+### 4. 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
-- 新生代使用:复制算法
-- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
+- 新生代使用:复制算法
+- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
-## 垃圾收集器
+## 垃圾收集器
-
+
-以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
+以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
-- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
-- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
+- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
+- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
-### 1. Serial 收集器
+### 1. Serial 收集器
-
+
-Serial 翻译为串行,也就是说它以串行的方式执行。
+Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
-它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
+它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
-它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
+它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
-### 2. ParNew 收集器
+### 2. ParNew 收集器
-
+
-它是 Serial 收集器的多线程版本。
+它是 Serial 收集器的多线程版本。
-它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
+它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
-### 3. Parallel Scavenge 收集器
+### 3. Parallel Scavenge 收集器
-与 ParNew 一样是多线程收集器。
+与 ParNew 一样是多线程收集器。
-其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
+其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
-停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
+停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
-可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
+可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
-### 4. Serial Old 收集器
+### 4. Serial Old 收集器
-
+
-是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
+是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
-- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
-- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
+- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
+- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
-### 5. Parallel Old 收集器
+### 5. Parallel Old 收集器
-
+
-是 Parallel Scavenge 收集器的老年代版本。
+是 Parallel Scavenge 收集器的老年代版本。
-在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
+在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
-### 6. CMS 收集器
+### 6. CMS 收集器
-
+
-CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
+CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
-- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
-- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
-- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
-- 并发清除:不需要停顿。
+- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
+- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
+- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
+- 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
-- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
-- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
-- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
+- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
+- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
+- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
-### 7. G1 收集器
+### 7. G1 收集器
-G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
+G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
-堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
+堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
-
+
-G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
+G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
-
+
-通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
+通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
-每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
+每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
-
+
-如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
+如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
-- 初始标记
-- 并发标记
-- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
-- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
+- 初始标记
+- 并发标记
+- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
+- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
-- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
-- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
+- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
+- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
-# 三、内存分配与回收策略
+# 三、内存分配与回收策略
-## Minor GC 和 Full GC
+## Minor GC 和 Full GC
-- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
+- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
-- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
+- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
-## 内存分配策略
+## 内存分配策略
-### 1. 对象优先在 Eden 分配
+### 1. 对象优先在 Eden 分配
-大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
+大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
-### 2. 大对象直接进入老年代
+### 2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
--XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
+-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
-### 3. 长期存活的对象进入老年代
+### 3. 长期存活的对象进入老年代
-为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
+为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
--XX:MaxTenuringThreshold 用来定义年龄的阈值。
+-XX:MaxTenuringThreshold 用来定义年龄的阈值。
-### 4. 动态对象年龄判定
+### 4. 动态对象年龄判定
-虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
+虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
-### 5. 空间分配担保
+### 5. 空间分配担保
-在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
+在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
-如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
+如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
-## Full GC 的触发条件
+## Full GC 的触发条件
-对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
+对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
-### 1. 调用 System.gc()
+### 1. 调用 System.gc()
-只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
+只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
-### 2. 老年代空间不足
+### 2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
-为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
+为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
-### 3. 空间分配担保失败
+### 3. 空间分配担保失败
-使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。
+使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。
-### 4. JDK 1.7 及以前的永久代空间不足
+### 4. JDK 1.7 及以前的永久代空间不足
-在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
+在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
-当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
+当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
-为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
+为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
-### 5. Concurrent Mode Failure
+### 5. Concurrent Mode Failure
-执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
+执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
-# 四、类加载机制
+# 四、类加载机制
类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
-## 类的生命周期
+## 类的生命周期
-
+
-包括以下 7 个阶段:
+包括以下 7 个阶段:
-- **加载(Loading)**
-- **验证(Verification)**
-- **准备(Preparation)**
-- **解析(Resolution)**
-- **初始化(Initialization)**
-- 使用(Using)
-- 卸载(Unloading)
+- **加载(Loading)**
+- **验证(Verification)**
+- **准备(Preparation)**
+- **解析(Resolution)**
+- **初始化(Initialization)**
+- 使用(Using)
+- 卸载(Unloading)
-## 类加载过程
+## 类加载过程
-包含了加载、验证、准备、解析和初始化这 5 个阶段。
+包含了加载、验证、准备、解析和初始化这 5 个阶段。
-### 1. 加载
+### 1. 加载
加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
-- 通过类的完全限定名称获取定义该类的二进制字节流。
-- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
-- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
+- 通过类的完全限定名称获取定义该类的二进制字节流。
+- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
+- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
-- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
-- 从网络中获取,最典型的应用是 Applet。
-- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
-- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
+- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
+- 从网络中获取,最典型的应用是 Applet。
+- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
+- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
-### 2. 验证
+### 2. 验证
-确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
+确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
-### 3. 准备
+### 3. 准备
-类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
+类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
-初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
+初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
```java
-public static int value = 123;
+public static int value = 123;
```
-如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。
+如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。
```java
-public static final int value = 123;
+public static final int value = 123;
```
-### 4. 解析
+### 4. 解析
将常量池的符号引用替换为直接引用的过程。
-其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
+其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
- <--">
+
-### 5. 初始化
+### 5. 初始化
-">
+
-初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
+初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
-<<<<<<< HEAD
-<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
-=======
<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
->>>>>>> 9f680db0cc99bd992c7f979442ecf458a33f9c1b
```java
-public class Test {
- static {
- i = 0; // 给变量赋值可以正常编译通过
- System.out.print(i); // 这句编译器会提示“非法向前引用”
- }
- static int i = 1;
+public class Test {
+ static {
+ i = 0; // 给变量赋值可以正常编译通过
+ System.out.print(i); // 这句编译器会提示“非法向前引用”
+ }
+ static int i = 1;
}
```
-由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
+由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
```java
-static class Parent {
- public static int A = 1;
- static {
- A = 2;
- }
+static class Parent {
+ public static int A = 1;
+ static {
+ A = 2;
+ }
}
-static class Sub extends Parent {
- public static int B = A;
+static class Sub extends Parent {
+ public static int B = A;
}
-public static void main(String[] args) {
- System.out.println(Sub.B); // 2
+public static void main(String[] args) {
+ System.out.println(Sub.B); // 2
}
```
-接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
+接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
-虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
+虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
-## 类初始化时机
+## 类初始化时机
-### 1. 主动引用
+### 1. 主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
-- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
+- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
-- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
+- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
-- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
+- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
+- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
-- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
+- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
-### 2. 被动引用
+### 2. 被动引用
-以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
+以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
-- 通过子类引用父类的静态字段,不会导致子类初始化。
+- 通过子类引用父类的静态字段,不会导致子类初始化。
```java
-System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
+System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
```
-- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
+- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
```java
-SuperClass[] sca = new SuperClass[10];
+SuperClass[] sca = new SuperClass[10];
```
-- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
+- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
```java
System.out.println(ConstClass.HELLOWORLD);
```
-## 类与类加载器
+## 类与类加载器
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
-这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
+这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
-## 类加载器分类
+## 类加载器分类
-从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
+从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
-- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
+- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
-- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
+- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
-从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
+从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
-- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
+- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
-- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
+- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
-- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
+- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
-
+
-## 双亲委派模型
+## 双亲委派模型
应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。
-下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
+下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
-
+
-### 1. 工作过程
+### 1. 工作过程
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
-### 2. 好处
+### 2. 好处
-使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
+使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
-例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
+例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
-### 3. 实现
+### 3. 实现
-以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
+以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
```java
-public abstract class ClassLoader {
- // The parent class loader for delegation
- private final ClassLoader parent;
+public abstract class ClassLoader {
+ // The parent class loader for delegation
+ private final ClassLoader parent;
- public Class> loadClass(String name) throws ClassNotFoundException {
- return loadClass(name, false);
- }
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ return loadClass(name, false);
+ }
- protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- synchronized (getClassLoadingLock(name)) {
- // First, check if the class has already been loaded
- Class> c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
+ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ synchronized (getClassLoadingLock(name)) {
+ // First, check if the class has already been loaded
+ Class> c = findLoadedClass(name);
+ if (c == null) {
+ try {
+ if (parent != null) {
+ c = parent.loadClass(name, false);
+ } else {
+ c = findBootstrapClassOrNull(name);
+ }
+ } catch (ClassNotFoundException e) {
+ // ClassNotFoundException thrown if class not found
+ // from the non-null parent class loader
+ }
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
+ if (c == null) {
+ // If still not found, then invoke findClass in order
+ // to find the class.
+ c = findClass(name);
+ }
+ }
+ if (resolve) {
+ resolveClass(c);
+ }
+ return c;
+ }
+ }
- protected Class> findClass(String name) throws ClassNotFoundException {
- throw new ClassNotFoundException(name);
- }
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ throw new ClassNotFoundException(name);
+ }
}
```
-## 自定义类加载器实现
+## 自定义类加载器实现
-以下代码中的 FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
+以下代码中的 FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
-java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
+java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
```java
-public class FileSystemClassLoader extends ClassLoader {
+public class FileSystemClassLoader extends ClassLoader {
- private String rootDir;
+ private String rootDir;
- public FileSystemClassLoader(String rootDir) {
- this.rootDir = rootDir;
- }
+ public FileSystemClassLoader(String rootDir) {
+ this.rootDir = rootDir;
+ }
- protected Class> findClass(String name) throws ClassNotFoundException {
- byte[] classData = getClassData(name);
- if (classData == null) {
- throw new ClassNotFoundException();
- } else {
- return defineClass(name, classData, 0, classData.length);
- }
- }
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ byte[] classData = getClassData(name);
+ if (classData == null) {
+ throw new ClassNotFoundException();
+ } else {
+ return defineClass(name, classData, 0, classData.length);
+ }
+ }
- private byte[] getClassData(String className) {
- String path = classNameToPath(className);
- try {
- InputStream ins = new FileInputStream(path);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- int bufferSize = 4096;
- byte[] buffer = new byte[bufferSize];
- int bytesNumRead;
- while ((bytesNumRead = ins.read(buffer)) != -1) {
- baos.write(buffer, 0, bytesNumRead);
- }
- return baos.toByteArray();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
+ private byte[] getClassData(String className) {
+ String path = classNameToPath(className);
+ try {
+ InputStream ins = new FileInputStream(path);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ int bufferSize = 4096;
+ byte[] buffer = new byte[bufferSize];
+ int bytesNumRead;
+ while ((bytesNumRead = ins.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesNumRead);
+ }
+ return baos.toByteArray();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
- private String classNameToPath(String className) {
- return rootDir + File.separatorChar
- + className.replace('.', File.separatorChar) + ".class";
- }
+ private String classNameToPath(String className) {
+ return rootDir + File.separatorChar
+ + className.replace('.', File.separatorChar) + ".class";
+ }
}
```
-# 参考资料
+# 参考资料
-- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
-- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
-- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
-[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
-- [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/)
-- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)
-- [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/)
-- [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271)
-- [深入理解 JVM(2)——GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/)
-- [深入理解 JVM(3)——7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/)
-- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)
-- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
-- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
-- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
----bottom---CyC---
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
+- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
+- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
+[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
+- [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/)
+- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)
+- [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/)
+- [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271)
+- [深入理解 JVM(2)——GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/)
+- [深入理解 JVM(3)——7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/)
+- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)
+- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
+- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
+- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 二分查找.md b/docs/notes/Leetcode 题解 - 二分查找.md
index 5f9da8de..72ae790f 100644
--- a/docs/notes/Leetcode 题解 - 二分查找.md
+++ b/docs/notes/Leetcode 题解 - 二分查找.md
@@ -1,277 +1,300 @@
-# 原理
+
+* [原理](#原理)
+ * [1. 正常实现](#1-正常实现)
+ * [2. 时间复杂度](#2-时间复杂度)
+ * [3. m 计算](#3-m-计算)
+ * [4. 返回值](#4-返回值)
+ * [5. 变种](#5-变种)
+* [例题](#例题)
+ * [1. 求开方](#1-求开方)
+ * [2. 大于给定元素的最小元素](#2-大于给定元素的最小元素)
+ * [3. 有序数组的 Single Element](#3-有序数组的-single-element)
+ * [4. 第一个错误的版本](#4-第一个错误的版本)
+ * [5. 旋转数组的最小数字](#5-旋转数组的最小数字)
+ * [6. 查找区间](#6-查找区间)
+
-## 1. 正常实现
+
+# 原理
+
+## 1. 正常实现
```java
-public int binarySearch(int[] nums, int key) {
- int l = 0, h = nums.length - 1;
- while (l <= h) {
- int m = l + (h - l) / 2;
- if (nums[m] == key) {
- return m;
- } else if (nums[m] > key) {
- h = m - 1;
- } else {
- l = m + 1;
- }
- }
- return -1;
+public int binarySearch(int[] nums, int key) {
+ int l = 0, h = nums.length - 1;
+ while (l <= h) {
+ int m = l + (h - l) / 2;
+ if (nums[m] == key) {
+ return m;
+ } else if (nums[m] > key) {
+ h = m - 1;
+ } else {
+ l = m + 1;
+ }
+ }
+ return -1;
}
```
-## 2. 时间复杂度
+## 2. 时间复杂度
-二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。
+二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。
-## 3. m 计算
+## 3. m 计算
-有两种计算中值 m 的方式:
+有两种计算中值 m 的方式:
-- m = (l + h) / 2
-- m = l + (h - l) / 2
+- m = (l + h) / 2
+- m = l + (h - l) / 2
-l + h 可能出现加法溢出,最好使用第二种方式。
+l + h 可能出现加法溢出,最好使用第二种方式。
-## 4. 返回值
+## 4. 返回值
-循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值:
+循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值:
-- -1:以一个错误码表示没有查找到 key
-- l:将 key 插入到 nums 中的正确位置
+- -1:以一个错误码表示没有查找到 key
+- l:将 key 插入到 nums 中的正确位置
-## 5. 变种
+## 5. 变种
-二分查找可以有很多变种,变种实现要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下:
+二分查找可以有很多变种,变种实现要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下:
```java
-public int binarySearch(int[] nums, int key) {
- int l = 0, h = nums.length - 1;
- while (l < h) {
- int m = l + (h - l) / 2;
- if (nums[m] >= key) {
- h = m;
- } else {
- l = m + 1;
- }
- }
- return l;
+public int binarySearch(int[] nums, int key) {
+ int l = 0, h = nums.length - 1;
+ while (l < h) {
+ int m = l + (h - l) / 2;
+ if (nums[m] >= key) {
+ h = m;
+ } else {
+ l = m + 1;
+ }
+ }
+ return l;
}
```
该实现和正常实现有以下不同:
-- 循环条件为 l < h
-- h 的赋值表达式为 h = m
-- 最后返回 l 而不是 -1
+- 循环条件为 l < h
+- h 的赋值表达式为 h = m
+- 最后返回 l 而不是 -1
-在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。
+在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。
-在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况:
+在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况:
```text
-nums = {0, 1, 2}, key = 1
-l m h
-0 1 2 nums[m] >= key
-0 0 1 nums[m] < key
-1 1 1 nums[m] >= key
-1 1 1 nums[m] >= key
+nums = {0, 1, 2}, key = 1
+l m h
+0 1 2 nums[m] >= key
+0 0 1 nums[m] < key
+1 1 1 nums[m] >= key
+1 1 1 nums[m] >= key
...
```
-当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。
+当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。
-# 例题
+# 例题
-## 1. 求开方
+## 1. 求开方
-[69. Sqrt(x) (Easy)](https://leetcode.com/problems/sqrtx/description/)
+[69. Sqrt(x) (Easy)](https://leetcode.com/problems/sqrtx/description/)
```html
-Input: 4
-Output: 2
+Input: 4
+Output: 2
-Input: 8
-Output: 2
-Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated.
+Input: 8
+Output: 2
+Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated.
```
-一个数 x 的开方 sqrt 一定在 0 ~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 ~ x 之间查找 sqrt。
+一个数 x 的开方 sqrt 一定在 0 \~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 \~ x 之间查找 sqrt。
-对于 x = 8,它的开方是 2.82842...,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。
+对于 x = 8,它的开方是 2.82842...,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。
```java
-public int mySqrt(int x) {
- if (x <= 1) {
- return x;
- }
- int l = 1, h = x;
- while (l <= h) {
- int mid = l + (h - l) / 2;
- int sqrt = x / mid;
- if (sqrt == mid) {
- return mid;
- } else if (mid > sqrt) {
- h = mid - 1;
- } else {
- l = mid + 1;
- }
- }
- return h;
+public int mySqrt(int x) {
+ if (x <= 1) {
+ return x;
+ }
+ int l = 1, h = x;
+ while (l <= h) {
+ int mid = l + (h - l) / 2;
+ int sqrt = x / mid;
+ if (sqrt == mid) {
+ return mid;
+ } else if (mid > sqrt) {
+ h = mid - 1;
+ } else {
+ l = mid + 1;
+ }
+ }
+ return h;
}
```
-## 2. 大于给定元素的最小元素
+## 2. 大于给定元素的最小元素
-[744. Find Smallest Letter Greater Than Target (Easy)](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/)
+[744. Find Smallest Letter Greater Than Target (Easy)](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/)
```html
Input:
-letters = ["c", "f", "j"]
-target = "d"
-Output: "f"
+letters = ["c", "f", "j"]
+target = "d"
+Output: "f"
Input:
-letters = ["c", "f", "j"]
-target = "k"
-Output: "c"
+letters = ["c", "f", "j"]
+target = "k"
+Output: "c"
```
-题目描述:给定一个有序的字符数组 letters 和一个字符 target,要求找出 letters 中大于 target 的最小字符,如果找不到就返回第 1 个字符。
+题目描述:给定一个有序的字符数组 letters 和一个字符 target,要求找出 letters 中大于 target 的最小字符,如果找不到就返回第 1 个字符。
```java
-public char nextGreatestLetter(char[] letters, char target) {
- int n = letters.length;
- int l = 0, h = n - 1;
- while (l <= h) {
- int m = l + (h - l) / 2;
- if (letters[m] <= target) {
- l = m + 1;
- } else {
- h = m - 1;
- }
- }
- return l < n ? letters[l] : letters[0];
+public char nextGreatestLetter(char[] letters, char target) {
+ int n = letters.length;
+ int l = 0, h = n - 1;
+ while (l <= h) {
+ int m = l + (h - l) / 2;
+ if (letters[m] <= target) {
+ l = m + 1;
+ } else {
+ h = m - 1;
+ }
+ }
+ return l < n ? letters[l] : letters[0];
}
```
-## 3. 有序数组的 Single Element
+## 3. 有序数组的 Single Element
-[540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/)
+[540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/)
```html
-Input: [1, 1, 2, 3, 3, 4, 4, 8, 8]
-Output: 2
+Input: [1, 1, 2, 3, 3, 4, 4, 8, 8]
+Output: 2
```
-题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。
+题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。
-令 index 为 Single Element 在数组中的位置。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。
+令 index 为 Single Element 在数组中的位置。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。
-从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。
+从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。
-因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。
+因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。
```java
-public int singleNonDuplicate(int[] nums) {
- int l = 0, h = nums.length - 1;
- while (l < h) {
- int m = l + (h - l) / 2;
- if (m % 2 == 1) {
- m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数
- }
- if (nums[m] == nums[m + 1]) {
- l = m + 2;
- } else {
- h = m;
- }
- }
- return nums[l];
+public int singleNonDuplicate(int[] nums) {
+ int l = 0, h = nums.length - 1;
+ while (l < h) {
+ int m = l + (h - l) / 2;
+ if (m % 2 == 1) {
+ m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数
+ }
+ if (nums[m] == nums[m + 1]) {
+ l = m + 2;
+ } else {
+ h = m;
+ }
+ }
+ return nums[l];
}
```
-## 4. 第一个错误的版本
+## 4. 第一个错误的版本
-[278. First Bad Version (Easy)](https://leetcode.com/problems/first-bad-version/description/)
+[278. First Bad Version (Easy)](https://leetcode.com/problems/first-bad-version/description/)
-题目描述:给定一个元素 n 代表有 [1, 2, ..., n] 版本,可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。
+题目描述:给定一个元素 n 代表有 [1, 2, ..., n] 版本,可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。
-如果第 m 个版本出错,则表示第一个错误的版本在 [l, m] 之间,令 h = m;否则第一个错误的版本在 [m + 1, h] 之间,令 l = m + 1。
+如果第 m 个版本出错,则表示第一个错误的版本在 [l, m] 之间,令 h = m;否则第一个错误的版本在 [m + 1, h] 之间,令 l = m + 1。
-因为 h 的赋值表达式为 h = m,因此循环条件为 l < h。
+因为 h 的赋值表达式为 h = m,因此循环条件为 l < h。
```java
-public int firstBadVersion(int n) {
- int l = 1, h = n;
- while (l < h) {
- int mid = l + (h - l) / 2;
- if (isBadVersion(mid)) {
- h = mid;
- } else {
- l = mid + 1;
- }
- }
- return l;
+public int firstBadVersion(int n) {
+ int l = 1, h = n;
+ while (l < h) {
+ int mid = l + (h - l) / 2;
+ if (isBadVersion(mid)) {
+ h = mid;
+ } else {
+ l = mid + 1;
+ }
+ }
+ return l;
}
```
-## 5. 旋转数组的最小数字
+## 5. 旋转数组的最小数字
-[153. Find Minimum in Rotated Sorted Array (Medium)](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/)
+[153. Find Minimum in Rotated Sorted Array (Medium)](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/)
```html
-Input: [3,4,5,1,2],
-Output: 1
+Input: [3,4,5,1,2],
+Output: 1
```
```java
-public int findMin(int[] nums) {
- int l = 0, h = nums.length - 1;
- while (l < h) {
- int m = l + (h - l) / 2;
- if (nums[m] <= nums[h]) {
- h = m;
- } else {
- l = m + 1;
- }
- }
- return nums[l];
+public int findMin(int[] nums) {
+ int l = 0, h = nums.length - 1;
+ while (l < h) {
+ int m = l + (h - l) / 2;
+ if (nums[m] <= nums[h]) {
+ h = m;
+ } else {
+ l = m + 1;
+ }
+ }
+ return nums[l];
}
```
-## 6. 查找区间
+## 6. 查找区间
-[34. Search for a Range (Medium)](https://leetcode.com/problems/search-for-a-range/description/)
+[34. Search for a Range (Medium)](https://leetcode.com/problems/search-for-a-range/description/)
```html
-Input: nums = [5,7,7,8,8,10], target = 8
-Output: [3,4]
+Input: nums = [5,7,7,8,8,10], target = 8
+Output: [3,4]
-Input: nums = [5,7,7,8,8,10], target = 6
-Output: [-1,-1]
+Input: nums = [5,7,7,8,8,10], target = 6
+Output: [-1,-1]
```
```java
-public int[] searchRange(int[] nums, int target) {
- int first = binarySearch(nums, target);
- int last = binarySearch(nums, target + 1) - 1;
- if (first == nums.length || nums[first] != target) {
- return new int[]{-1, -1};
- } else {
- return new int[]{first, Math.max(first, last)};
- }
+public int[] searchRange(int[] nums, int target) {
+ int first = binarySearch(nums, target);
+ int last = binarySearch(nums, target + 1) - 1;
+ if (first == nums.length || nums[first] != target) {
+ return new int[]{-1, -1};
+ } else {
+ return new int[]{first, Math.max(first, last)};
+ }
}
-private int binarySearch(int[] nums, int target) {
- int l = 0, h = nums.length; // 注意 h 的初始值
- while (l < h) {
- int m = l + (h - l) / 2;
- if (nums[m] >= target) {
- h = m;
- } else {
- l = m + 1;
- }
- }
- return l;
+private int binarySearch(int[] nums, int target) {
+ int l = 0, h = nums.length; // 注意 h 的初始值
+ while (l < h) {
+ int m = l + (h - l) / 2;
+ if (nums[m] >= target) {
+ h = m;
+ } else {
+ l = m + 1;
+ }
+ }
+ return l;
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 位运算.md b/docs/notes/Leetcode 题解 - 位运算.md
index 08f7f3fe..f9f1af46 100644
--- a/docs/notes/Leetcode 题解 - 位运算.md
+++ b/docs/notes/Leetcode 题解 - 位运算.md
@@ -1,405 +1,433 @@
-# 原理
+
+* [原理](#原理)
+ * [1. 基本原理](#1-基本原理)
+ * [2. mask 计算](#2-mask-计算)
+ * [3. Java 中的位操作](#3-java-中的位操作)
+* [例题](#例题)
+ * [统计两个数的二进制表示有多少位不同](#统计两个数的二进制表示有多少位不同)
+ * [数组中唯一一个不重复的元素](#数组中唯一一个不重复的元素)
+ * [找出数组中缺失的那个数](#找出数组中缺失的那个数)
+ * [数组中不重复的两个元素](#数组中不重复的两个元素)
+ * [翻转一个数的比特位](#翻转一个数的比特位)
+ * [不用额外变量交换两个整数](#不用额外变量交换两个整数)
+ * [判断一个数是不是 2 的 n 次方](#判断一个数是不是-2-的-n-次方)
+ * [判断一个数是不是 4 的 n 次方](#判断一个数是不是-4-的-n-次方)
+ * [判断一个数的位级表示是否不会出现连续的 0 和 1](#判断一个数的位级表示是否不会出现连续的-0-和-1)
+ * [求一个数的补码](#求一个数的补码)
+ * [实现整数的加法](#实现整数的加法)
+ * [字符串数组最大乘积](#字符串数组最大乘积)
+ * [统计从 0 \~ n 每个数的二进制表示中 1 的个数](#统计从-0-\~-n-每个数的二进制表示中-1-的个数)
+
-## 1. 基本原理
-0s 表示一串 0,1s 表示一串 1。
+# 原理
+
+## 1. 基本原理
+
+0s 表示一串 0,1s 表示一串 1。
```
-x ^ 0s = x x & 0s = 0 x | 0s = x
-x ^ 1s = ~x x & 1s = x x | 1s = 1s
-x ^ x = 0 x & x = x x | x = x
+x ^ 0s = x x & 0s = 0 x | 0s = x
+x ^ 1s = ~x x & 1s = x x | 1s = 1s
+x ^ x = 0 x & x = x x | x = x
```
-- 利用 x ^ 1s = ~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。
-- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。
-- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。
+- 利用 x ^ 1s = \~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。
+- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。
+- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。
位与运算技巧:
-- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110100,减去 1 得到 10110011,这两个数相与得到 10110000。
-- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110100,-n 得到 01001100,相与得到 00000100。
-- n-n&(~n+1) 去除 n 的位级表示中最高的那一位。
+- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110100,减去 1 得到 10110011,这两个数相与得到 10110000。
+- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110100,-n 得到 01001100,相与得到 00000100。
+- n-n&(\~n+1) 去除 n 的位级表示中最高的那一位。
移位运算:
-- \>\> n 为算术右移,相当于除以 2n ;
-- \>\>\> n 为无符号右移,左边会补上 0。
-- << n 为算术左移,相当于乘以 2n 。
+- \>\> n 为算术右移,相当于除以 2n ;
+- \>\>\> n 为无符号右移,左边会补上 0。
+- << n 为算术左移,相当于乘以 2n 。
-## 2. mask 计算
+## 2. mask 计算
-要获取 111111111,将 0 取反即可,~0。
+要获取 111111111,将 0 取反即可,\~0。
-要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。
+要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。
-要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。
+要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。
-要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 ~(1<<(i+1)-1)。
+要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 \~(1<<(i+1)-1)。
-## 3. Java 中的位操作
+## 3. Java 中的位操作
```html
-static int Integer.bitCount(); // 统计 1 的数量
-static int Integer.highestOneBit(); // 获得最高位
-static String toBinaryString(int i); // 转换为二进制表示的字符串
+static int Integer.bitCount(); // 统计 1 的数量
+static int Integer.highestOneBit(); // 获得最高位
+static String toBinaryString(int i); // 转换为二进制表示的字符串
```
-# 例题
+# 例题
-## 统计两个数的二进制表示有多少位不同
+## 统计两个数的二进制表示有多少位不同
-[461. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/)
+[461. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/)
```html
-Input: x = 1, y = 4
+Input: x = 1, y = 4
-Output: 2
+Output: 2
Explanation:
-1 (0 0 0 1)
-4 (0 1 0 0)
- ↑ ↑
+1 (0 0 0 1)
+4 (0 1 0 0)
+ ↑ ↑
-The above arrows point to positions where the corresponding bits are different.
+The above arrows point to positions where the corresponding bits are different.
```
-对两个数进行异或操作,位级表示不同的那一位为 1,统计有多少个 1 即可。
+对两个数进行异或操作,位级表示不同的那一位为 1,统计有多少个 1 即可。
```java
-public int hammingDistance(int x, int y) {
- int z = x ^ y;
- int cnt = 0;
- while(z != 0) {
- if ((z & 1) == 1) cnt++;
- z = z >> 1;
- }
- return cnt;
+public int hammingDistance(int x, int y) {
+ int z = x ^ y;
+ int cnt = 0;
+ while(z != 0) {
+ if ((z & 1) == 1) cnt++;
+ z = z >> 1;
+ }
+ return cnt;
}
```
-使用 z&(z-1) 去除 z 位级表示最低的那一位。
+使用 z&(z-1) 去除 z 位级表示最低的那一位。
```java
-public int hammingDistance(int x, int y) {
- int z = x ^ y;
- int cnt = 0;
- while (z != 0) {
- z &= (z - 1);
- cnt++;
- }
- return cnt;
+public int hammingDistance(int x, int y) {
+ int z = x ^ y;
+ int cnt = 0;
+ while (z != 0) {
+ z &= (z - 1);
+ cnt++;
+ }
+ return cnt;
}
```
-可以使用 Integer.bitcount() 来统计 1 个的个数。
+可以使用 Integer.bitcount() 来统计 1 个的个数。
```java
-public int hammingDistance(int x, int y) {
- return Integer.bitCount(x ^ y);
+public int hammingDistance(int x, int y) {
+ return Integer.bitCount(x ^ y);
}
```
-## 数组中唯一一个不重复的元素
+## 数组中唯一一个不重复的元素
-[136. Single Number (Easy)](https://leetcode.com/problems/single-number/description/)
+[136. Single Number (Easy)](https://leetcode.com/problems/single-number/description/)
```html
-Input: [4,1,2,1,2]
-Output: 4
+Input: [4,1,2,1,2]
+Output: 4
```
-两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。
+两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。
```java
-public int singleNumber(int[] nums) {
- int ret = 0;
- for (int n : nums) ret = ret ^ n;
- return ret;
+public int singleNumber(int[] nums) {
+ int ret = 0;
+ for (int n : nums) ret = ret ^ n;
+ return ret;
}
```
-## 找出数组中缺失的那个数
+## 找出数组中缺失的那个数
-[268. Missing Number (Easy)](https://leetcode.com/problems/missing-number/description/)
+[268. Missing Number (Easy)](https://leetcode.com/problems/missing-number/description/)
```html
-Input: [3,0,1]
-Output: 2
+Input: [3,0,1]
+Output: 2
```
-题目描述:数组元素在 0-n 之间,但是有一个数是缺失的,要求找到这个缺失的数。
+题目描述:数组元素在 0-n 之间,但是有一个数是缺失的,要求找到这个缺失的数。
```java
-public int missingNumber(int[] nums) {
- int ret = 0;
- for (int i = 0; i < nums.length; i++) {
- ret = ret ^ i ^ nums[i];
- }
- return ret ^ nums.length;
+public int missingNumber(int[] nums) {
+ int ret = 0;
+ for (int i = 0; i < nums.length; i++) {
+ ret = ret ^ i ^ nums[i];
+ }
+ return ret ^ nums.length;
}
```
-## 数组中不重复的两个元素
+## 数组中不重复的两个元素
-[260. Single Number III (Medium)](https://leetcode.com/problems/single-number-iii/description/)
+[260. Single Number III (Medium)](https://leetcode.com/problems/single-number-iii/description/)
两个不相等的元素在位级表示上必定会有一位存在不同。
将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。
-diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。
+diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。
```java
-public int[] singleNumber(int[] nums) {
- int diff = 0;
- for (int num : nums) diff ^= num;
- diff &= -diff; // 得到最右一位
- int[] ret = new int[2];
- for (int num : nums) {
- if ((num & diff) == 0) ret[0] ^= num;
- else ret[1] ^= num;
- }
- return ret;
+public int[] singleNumber(int[] nums) {
+ int diff = 0;
+ for (int num : nums) diff ^= num;
+ diff &= -diff; // 得到最右一位
+ int[] ret = new int[2];
+ for (int num : nums) {
+ if ((num & diff) == 0) ret[0] ^= num;
+ else ret[1] ^= num;
+ }
+ return ret;
}
```
-## 翻转一个数的比特位
+## 翻转一个数的比特位
-[190. Reverse Bits (Easy)](https://leetcode.com/problems/reverse-bits/description/)
+[190. Reverse Bits (Easy)](https://leetcode.com/problems/reverse-bits/description/)
```java
-public int reverseBits(int n) {
- int ret = 0;
- for (int i = 0; i < 32; i++) {
- ret <<= 1;
- ret |= (n & 1);
- n >>>= 1;
- }
- return ret;
+public int reverseBits(int n) {
+ int ret = 0;
+ for (int i = 0; i < 32; i++) {
+ ret <<= 1;
+ ret |= (n & 1);
+ n >>>= 1;
+ }
+ return ret;
}
```
-如果该函数需要被调用很多次,可以将 int 拆成 4 个 byte,然后缓存 byte 对应的比特位翻转,最后再拼接起来。
+如果该函数需要被调用很多次,可以将 int 拆成 4 个 byte,然后缓存 byte 对应的比特位翻转,最后再拼接起来。
```java
-private static Map cache = new HashMap<>();
+private static Map cache = new HashMap<>();
-public int reverseBits(int n) {
- int ret = 0;
- for (int i = 0; i < 4; i++) {
- ret <<= 8;
- ret |= reverseByte((byte) (n & 0b11111111));
- n >>= 8;
- }
- return ret;
+public int reverseBits(int n) {
+ int ret = 0;
+ for (int i = 0; i < 4; i++) {
+ ret <<= 8;
+ ret |= reverseByte((byte) (n & 0b11111111));
+ n >>= 8;
+ }
+ return ret;
}
-private int reverseByte(byte b) {
- if (cache.containsKey(b)) return cache.get(b);
- int ret = 0;
- byte t = b;
- for (int i = 0; i < 8; i++) {
- ret <<= 1;
- ret |= t & 1;
- t >>= 1;
- }
- cache.put(b, ret);
- return ret;
+private int reverseByte(byte b) {
+ if (cache.containsKey(b)) return cache.get(b);
+ int ret = 0;
+ byte t = b;
+ for (int i = 0; i < 8; i++) {
+ ret <<= 1;
+ ret |= t & 1;
+ t >>= 1;
+ }
+ cache.put(b, ret);
+ return ret;
}
```
-## 不用额外变量交换两个整数
+## 不用额外变量交换两个整数
-[程序员代码面试指南 :P317](#)
+[程序员代码面试指南 :P317](#)
```java
-a = a ^ b;
-b = a ^ b;
-a = a ^ b;
+a = a ^ b;
+b = a ^ b;
+a = a ^ b;
```
-## 判断一个数是不是 2 的 n 次方
+## 判断一个数是不是 2 的 n 次方
-[231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/)
+[231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/)
-二进制表示只有一个 1 存在。
+二进制表示只有一个 1 存在。
```java
-public boolean isPowerOfTwo(int n) {
- return n > 0 && Integer.bitCount(n) == 1;
+public boolean isPowerOfTwo(int n) {
+ return n > 0 && Integer.bitCount(n) == 1;
}
```
-利用 1000 & 0111 == 0 这种性质,得到以下解法:
+利用 1000 & 0111 == 0 这种性质,得到以下解法:
```java
-public boolean isPowerOfTwo(int n) {
- return n > 0 && (n & (n - 1)) == 0;
+public boolean isPowerOfTwo(int n) {
+ return n > 0 && (n & (n - 1)) == 0;
}
```
-## 判断一个数是不是 4 的 n 次方
+## 判断一个数是不是 4 的 n 次方
-[342. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/)
+[342. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/)
-这种数在二进制表示中有且只有一个奇数位为 1,例如 16(10000)。
+这种数在二进制表示中有且只有一个奇数位为 1,例如 16(10000)。
```java
-public boolean isPowerOfFour(int num) {
- return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0;
+public boolean isPowerOfFour(int num) {
+ return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0;
}
```
也可以使用正则表达式进行匹配。
```java
-public boolean isPowerOfFour(int num) {
- return Integer.toString(num, 4).matches("10*");
+public boolean isPowerOfFour(int num) {
+ return Integer.toString(num, 4).matches("10*");
}
```
-## 判断一个数的位级表示是否不会出现连续的 0 和 1
+## 判断一个数的位级表示是否不会出现连续的 0 和 1
-[693. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/)
+[693. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/)
```html
-Input: 10
-Output: True
+Input: 10
+Output: True
Explanation:
-The binary representation of 10 is: 1010.
+The binary representation of 10 is: 1010.
-Input: 11
-Output: False
+Input: 11
+Output: False
Explanation:
-The binary representation of 11 is: 1011.
+The binary representation of 11 is: 1011.
```
-对于 1010 这种位级表示的数,把它向右移动 1 位得到 101,这两个数每个位都不同,因此异或得到的结果为 1111。
+对于 1010 这种位级表示的数,把它向右移动 1 位得到 101,这两个数每个位都不同,因此异或得到的结果为 1111。
```java
-public boolean hasAlternatingBits(int n) {
- int a = (n ^ (n >> 1));
- return (a & (a + 1)) == 0;
+public boolean hasAlternatingBits(int n) {
+ int a = (n ^ (n >> 1));
+ return (a & (a + 1)) == 0;
}
```
-## 求一个数的补码
+## 求一个数的补码
-[476. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/)
+[476. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/)
```html
-Input: 5
-Output: 2
-Explanation: The binary representation of 5 is 101 (no leading zero bits), and its complement is 010. So you need to output 2.
+Input: 5
+Output: 2
+Explanation: The binary representation of 5 is 101 (no leading zero bits), and its complement is 010. So you need to output 2.
```
-题目描述:不考虑二进制表示中的首 0 部分。
+题目描述:不考虑二进制表示中的首 0 部分。
-对于 00000101,要求补码可以将它与 00000111 进行异或操作。那么问题就转换为求掩码 00000111。
+对于 00000101,要求补码可以将它与 00000111 进行异或操作。那么问题就转换为求掩码 00000111。
```java
-public int findComplement(int num) {
- if (num == 0) return 1;
- int mask = 1 << 30;
- while ((num & mask) == 0) mask >>= 1;
- mask = (mask << 1) - 1;
- return num ^ mask;
+public int findComplement(int num) {
+ if (num == 0) return 1;
+ int mask = 1 << 30;
+ while ((num & mask) == 0) mask >>= 1;
+ mask = (mask << 1) - 1;
+ return num ^ mask;
}
```
-可以利用 Java 的 Integer.highestOneBit() 方法来获得含有首 1 的数。
+可以利用 Java 的 Integer.highestOneBit() 方法来获得含有首 1 的数。
```java
-public int findComplement(int num) {
- if (num == 0) return 1;
- int mask = Integer.highestOneBit(num);
- mask = (mask << 1) - 1;
- return num ^ mask;
+public int findComplement(int num) {
+ if (num == 0) return 1;
+ int mask = Integer.highestOneBit(num);
+ mask = (mask << 1) - 1;
+ return num ^ mask;
}
```
-对于 10000000 这样的数要扩展成 11111111,可以利用以下方法:
+对于 10000000 这样的数要扩展成 11111111,可以利用以下方法:
```html
-mask |= mask >> 1 11000000
-mask |= mask >> 2 11110000
-mask |= mask >> 4 11111111
+mask |= mask >> 1 11000000
+mask |= mask >> 2 11110000
+mask |= mask >> 4 11111111
```
```java
-public int findComplement(int num) {
- int mask = num;
- mask |= mask >> 1;
- mask |= mask >> 2;
- mask |= mask >> 4;
- mask |= mask >> 8;
- mask |= mask >> 16;
- return (mask ^ num);
+public int findComplement(int num) {
+ int mask = num;
+ mask |= mask >> 1;
+ mask |= mask >> 2;
+ mask |= mask >> 4;
+ mask |= mask >> 8;
+ mask |= mask >> 16;
+ return (mask ^ num);
}
```
-## 实现整数的加法
+## 实现整数的加法
-[371. Sum of Two Integers (Easy)](https://leetcode.com/problems/sum-of-two-integers/description/)
+[371. Sum of Two Integers (Easy)](https://leetcode.com/problems/sum-of-two-integers/description/)
-a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。
+a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。
-递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。
+递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。
```java
-public int getSum(int a, int b) {
- return b == 0 ? a : getSum((a ^ b), (a & b) << 1);
+public int getSum(int a, int b) {
+ return b == 0 ? a : getSum((a ^ b), (a & b) << 1);
}
```
-## 字符串数组最大乘积
+## 字符串数组最大乘积
-[318. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/)
+[318. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/)
```html
-Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"]
-Return 16
-The two words can be "abcw", "xtfn".
+Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"]
+Return 16
+The two words can be "abcw", "xtfn".
```
题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。
-本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。
+本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。
```java
-public int maxProduct(String[] words) {
- int n = words.length;
- int[] val = new int[n];
- for (int i = 0; i < n; i++) {
- for (char c : words[i].toCharArray()) {
- val[i] |= 1 << (c - 'a');
- }
- }
- int ret = 0;
- for (int i = 0; i < n; i++) {
- for (int j = i + 1; j < n; j++) {
- if ((val[i] & val[j]) == 0) {
- ret = Math.max(ret, words[i].length() * words[j].length());
- }
- }
- }
- return ret;
+public int maxProduct(String[] words) {
+ int n = words.length;
+ int[] val = new int[n];
+ for (int i = 0; i < n; i++) {
+ for (char c : words[i].toCharArray()) {
+ val[i] |= 1 << (c - 'a');
+ }
+ }
+ int ret = 0;
+ for (int i = 0; i < n; i++) {
+ for (int j = i + 1; j < n; j++) {
+ if ((val[i] & val[j]) == 0) {
+ ret = Math.max(ret, words[i].length() * words[j].length());
+ }
+ }
+ }
+ return ret;
}
```
-## 统计从 0 ~ n 每个数的二进制表示中 1 的个数
+## 统计从 0 \~ n 每个数的二进制表示中 1 的个数
-[338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/)
+[338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/)
-对于数字 6(110),它可以看成是 4(100) 再加一个 2(10),因此 dp[i] = dp[i&(i-1)] + 1;
+对于数字 6(110),它可以看成是 4(100) 再加一个 2(10),因此 dp[i] = dp[i&(i-1)] + 1;
```java
-public int[] countBits(int num) {
- int[] ret = new int[num + 1];
- for(int i = 1; i <= num; i++){
- ret[i] = ret[i&(i-1)] + 1;
- }
- return ret;
+public int[] countBits(int num) {
+ int[] ret = new int[num + 1];
+ for(int i = 1; i <= num; i++){
+ ret[i] = ret[i&(i-1)] + 1;
+ }
+ return ret;
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 分治.md b/docs/notes/Leetcode 题解 - 分治.md
index 9f86e0d6..7c37b9fa 100644
--- a/docs/notes/Leetcode 题解 - 分治.md
+++ b/docs/notes/Leetcode 题解 - 分治.md
@@ -1,44 +1,55 @@
-# 1. 给表达式加括号
+
+* [1. 给表达式加括号](#1-给表达式加括号)
+
-[241. Different Ways to Add Parentheses (Medium)](https://leetcode.com/problems/different-ways-to-add-parentheses/description/)
+
+# 1. 给表达式加括号
+
+[241. Different Ways to Add Parentheses (Medium)](https://leetcode.com/problems/different-ways-to-add-parentheses/description/)
```html
-Input: "2-1-1".
+Input: "2-1-1".
-((2-1)-1) = 0
-(2-(1-1)) = 2
+((2-1)-1) = 0
+(2-(1-1)) = 2
-Output : [0, 2]
+Output : [0, 2]
```
```java
-public List diffWaysToCompute(String input) {
- List ways = new ArrayList<>();
- for (int i = 0; i < input.length(); i++) {
- char c = input.charAt(i);
- if (c == '+' || c == '-' || c == '*') {
- List left = diffWaysToCompute(input.substring(0, i));
- List right = diffWaysToCompute(input.substring(i + 1));
- for (int l : left) {
- for (int r : right) {
- switch (c) {
- case '+':
- ways.add(l + r);
- break;
- case '-':
- ways.add(l - r);
- break;
- case '*':
- ways.add(l * r);
- break;
- }
- }
- }
- }
- }
- if (ways.size() == 0) {
- ways.add(Integer.valueOf(input));
- }
- return ways;
+public List diffWaysToCompute(String input) {
+ List ways = new ArrayList<>();
+ for (int i = 0; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (c == '+' || c == '-' || c == '*') {
+ List left = diffWaysToCompute(input.substring(0, i));
+ List right = diffWaysToCompute(input.substring(i + 1));
+ for (int l : left) {
+ for (int r : right) {
+ switch (c) {
+ case '+':
+ ways.add(l + r);
+ break;
+ case '-':
+ ways.add(l - r);
+ break;
+ case '*':
+ ways.add(l * r);
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (ways.size() == 0) {
+ ways.add(Integer.valueOf(input));
+ }
+ return ways;
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 动态规划.md b/docs/notes/Leetcode 题解 - 动态规划.md
index 3b700152..e9b867c8 100644
--- a/docs/notes/Leetcode 题解 - 动态规划.md
+++ b/docs/notes/Leetcode 题解 - 动态规划.md
@@ -1,1176 +1,1227 @@
+
+* [斐波那契数列](#斐波那契数列)
+ * [爬楼梯](#爬楼梯)
+ * [强盗抢劫](#强盗抢劫)
+ * [强盗在环形街区抢劫](#强盗在环形街区抢劫)
+ * [信件错排](#信件错排)
+ * [母牛生产](#母牛生产)
+* [矩阵路径](#矩阵路径)
+ * [矩阵的最小路径和](#矩阵的最小路径和)
+ * [矩阵的总路径数](#矩阵的总路径数)
+* [数组区间](#数组区间)
+ * [数组区间和](#数组区间和)
+ * [数组中等差递增子区间的个数](#数组中等差递增子区间的个数)
+* [分割整数](#分割整数)
+ * [分割整数的最大乘积](#分割整数的最大乘积)
+ * [按平方数来分割整数](#按平方数来分割整数)
+ * [分割整数构成字母字符串](#分割整数构成字母字符串)
+* [最长递增子序列](#最长递增子序列)
+ * [最长递增子序列](#最长递增子序列)
+ * [一组整数对能够构成的最长链](#一组整数对能够构成的最长链)
+ * [最长摆动子序列](#最长摆动子序列)
+* [最长公共子序列](#最长公共子序列)
+* [0-1 背包](#0-1-背包)
+ * [空间优化](#空间优化)
+ * [无法使用贪心算法的解释](#无法使用贪心算法的解释)
+ * [变种](#变种)
+ * [划分数组为和相等的两部分](#划分数组为和相等的两部分)
+ * [改变一组数的正负号使得它们的和为一给定数](#改变一组数的正负号使得它们的和为一给定数)
+ * [01 字符构成最多的字符串](#01-字符构成最多的字符串)
+ * [找零钱的最少硬币数](#找零钱的最少硬币数)
+ * [找零钱的硬币数组合](#找零钱的硬币数组合)
+ * [字符串按单词列表分割](#字符串按单词列表分割)
+ * [组合总和](#组合总和)
+* [股票交易](#股票交易)
+ * [需要冷却期的股票交易](#需要冷却期的股票交易)
+ * [需要交易费用的股票交易](#需要交易费用的股票交易)
+ * [只能进行两次的股票交易](#只能进行两次的股票交易)
+ * [只能进行 k 次的股票交易](#只能进行-k-次的股票交易)
+* [字符串编辑](#字符串编辑)
+ * [删除两个字符串的字符使它们相等](#删除两个字符串的字符使它们相等)
+ * [编辑距离](#编辑距离)
+ * [复制粘贴字符](#复制粘贴字符)
+
+
+
递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。
-# 斐波那契数列
+# 斐波那契数列
-## 爬楼梯
+## 爬楼梯
-[70. Climbing Stairs (Easy)](https://leetcode.com/problems/climbing-stairs/description/)
+[70. Climbing Stairs (Easy)](https://leetcode.com/problems/climbing-stairs/description/)
-题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
+题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
-定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。
+定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。
-第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
+第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
-
+
-
+
-考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。
+考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。
```java
-public int climbStairs(int n) {
- if (n <= 2) {
- return n;
- }
- int pre2 = 1, pre1 = 2;
- for (int i = 2; i < n; i++) {
- int cur = pre1 + pre2;
- pre2 = pre1;
- pre1 = cur;
- }
- return pre1;
+public int climbStairs(int n) {
+ if (n <= 2) {
+ return n;
+ }
+ int pre2 = 1, pre1 = 2;
+ for (int i = 2; i < n; i++) {
+ int cur = pre1 + pre2;
+ pre2 = pre1;
+ pre1 = cur;
+ }
+ return pre1;
}
```
-## 强盗抢劫
+## 强盗抢劫
-[198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/)
+[198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/)
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
-定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。
+定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。
-由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以
+由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以
-
+
-
+
```java
-public int rob(int[] nums) {
- int pre2 = 0, pre1 = 0;
- for (int i = 0; i < nums.length; i++) {
- int cur = Math.max(pre2 + nums[i], pre1);
- pre2 = pre1;
- pre1 = cur;
- }
- return pre1;
+public int rob(int[] nums) {
+ int pre2 = 0, pre1 = 0;
+ for (int i = 0; i < nums.length; i++) {
+ int cur = Math.max(pre2 + nums[i], pre1);
+ pre2 = pre1;
+ pre1 = cur;
+ }
+ return pre1;
}
```
-## 强盗在环形街区抢劫
+## 强盗在环形街区抢劫
-[213. House Robber II (Medium)](https://leetcode.com/problems/house-robber-ii/description/)
+[213. House Robber II (Medium)](https://leetcode.com/problems/house-robber-ii/description/)
```java
-public int rob(int[] nums) {
- if (nums == null || nums.length == 0) {
- return 0;
- }
- int n = nums.length;
- if (n == 1) {
- return nums[0];
- }
- return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1));
+public int rob(int[] nums) {
+ if (nums == null || nums.length == 0) {
+ return 0;
+ }
+ int n = nums.length;
+ if (n == 1) {
+ return nums[0];
+ }
+ return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1));
}
-private int rob(int[] nums, int first, int last) {
- int pre2 = 0, pre1 = 0;
- for (int i = first; i <= last; i++) {
- int cur = Math.max(pre1, pre2 + nums[i]);
- pre2 = pre1;
- pre1 = cur;
- }
- return pre1;
+private int rob(int[] nums, int first, int last) {
+ int pre2 = 0, pre1 = 0;
+ for (int i = first; i <= last; i++) {
+ int cur = Math.max(pre1, pre2 + nums[i]);
+ pre2 = pre1;
+ pre1 = cur;
+ }
+ return pre1;
}
```
-## 信件错排
+## 信件错排
-题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。
+题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。
-定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
+定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
-- i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。
-- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-1] 种错误装信方式。
+- i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。
+- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-1] 种错误装信方式。
综上所述,错误装信数量方式数量为:
-
+
-
+
-## 母牛生产
+## 母牛生产
[程序员代码面试指南-P181](#)
-题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
+题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
-第 i 年成熟的牛的数量为:
+第 i 年成熟的牛的数量为:
-
+
-
+
-# 矩阵路径
+# 矩阵路径
-## 矩阵的最小路径和
+## 矩阵的最小路径和
-[64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/)
+[64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/)
```html
[[1,3,1],
- [1,5,1],
- [4,2,1]]
-Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum.
+ [1,5,1],
+ [4,2,1]]
+Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum.
```
题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。
```java
-public int minPathSum(int[][] grid) {
- if (grid.length == 0 || grid[0].length == 0) {
- return 0;
- }
- int m = grid.length, n = grid[0].length;
- int[] dp = new int[n];
- for (int i = 0; i < m; i++) {
- for (int j = 0; j < n; j++) {
- if (j == 0) {
- dp[j] = dp[j]; // 只能从上侧走到该位置
- } else if (i == 0) {
- dp[j] = dp[j - 1]; // 只能从左侧走到该位置
- } else {
- dp[j] = Math.min(dp[j - 1], dp[j]);
- }
- dp[j] += grid[i][j];
- }
- }
- return dp[n - 1];
+public int minPathSum(int[][] grid) {
+ if (grid.length == 0 || grid[0].length == 0) {
+ return 0;
+ }
+ int m = grid.length, n = grid[0].length;
+ int[] dp = new int[n];
+ for (int i = 0; i < m; i++) {
+ for (int j = 0; j < n; j++) {
+ if (j == 0) {
+ dp[j] = dp[j]; // 只能从上侧走到该位置
+ } else if (i == 0) {
+ dp[j] = dp[j - 1]; // 只能从左侧走到该位置
+ } else {
+ dp[j] = Math.min(dp[j - 1], dp[j]);
+ }
+ dp[j] += grid[i][j];
+ }
+ }
+ return dp[n - 1];
}
```
-## 矩阵的总路径数
+## 矩阵的总路径数
-[62. Unique Paths (Medium)](https://leetcode.com/problems/unique-paths/description/)
+[62. Unique Paths (Medium)](https://leetcode.com/problems/unique-paths/description/)
题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。
-
+
```java
-public int uniquePaths(int m, int n) {
- int[] dp = new int[n];
- Arrays.fill(dp, 1);
- for (int i = 1; i < m; i++) {
- for (int j = 1; j < n; j++) {
- dp[j] = dp[j] + dp[j - 1];
- }
- }
- return dp[n - 1];
+public int uniquePaths(int m, int n) {
+ int[] dp = new int[n];
+ Arrays.fill(dp, 1);
+ for (int i = 1; i < m; i++) {
+ for (int j = 1; j < n; j++) {
+ dp[j] = dp[j] + dp[j - 1];
+ }
+ }
+ return dp[n - 1];
}
```
-也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 中取出 D 个位置的组合数量,这个问题的解为 C(S, D)。
+也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 中取出 D 个位置的组合数量,这个问题的解为 C(S, D)。
```java
-public int uniquePaths(int m, int n) {
- int S = m + n - 2; // 总共的移动次数
- int D = m - 1; // 向下的移动次数
- long ret = 1;
- for (int i = 1; i <= D; i++) {
- ret = ret * (S - D + i) / i;
- }
- return (int) ret;
+public int uniquePaths(int m, int n) {
+ int S = m + n - 2; // 总共的移动次数
+ int D = m - 1; // 向下的移动次数
+ long ret = 1;
+ for (int i = 1; i <= D; i++) {
+ ret = ret * (S - D + i) / i;
+ }
+ return (int) ret;
}
```
-# 数组区间
+# 数组区间
-## 数组区间和
+## 数组区间和
-[303. Range Sum Query - Immutable (Easy)](https://leetcode.com/problems/range-sum-query-immutable/description/)
+[303. Range Sum Query - Immutable (Easy)](https://leetcode.com/problems/range-sum-query-immutable/description/)
```html
-Given nums = [-2, 0, 3, -5, 2, -1]
+Given nums = [-2, 0, 3, -5, 2, -1]
-sumRange(0, 2) -> 1
-sumRange(2, 5) -> -1
-sumRange(0, 5) -> -3
+sumRange(0, 2) -> 1
+sumRange(2, 5) -> -1
+sumRange(0, 5) -> -3
```
-求区间 i ~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 ~ i - 1 的和。
+求区间 i \~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 \~ i - 1 的和。
```java
-class NumArray {
+class NumArray {
- private int[] sums;
+ private int[] sums;
- public NumArray(int[] nums) {
- sums = new int[nums.length + 1];
- for (int i = 1; i <= nums.length; i++) {
- sums[i] = sums[i - 1] + nums[i - 1];
- }
- }
+ public NumArray(int[] nums) {
+ sums = new int[nums.length + 1];
+ for (int i = 1; i <= nums.length; i++) {
+ sums[i] = sums[i - 1] + nums[i - 1];
+ }
+ }
- public int sumRange(int i, int j) {
- return sums[j + 1] - sums[i];
- }
+ public int sumRange(int i, int j) {
+ return sums[j + 1] - sums[i];
+ }
}
```
-## 数组中等差递增子区间的个数
+## 数组中等差递增子区间的个数
-[413. Arithmetic Slices (Medium)](https://leetcode.com/problems/arithmetic-slices/description/)
+[413. Arithmetic Slices (Medium)](https://leetcode.com/problems/arithmetic-slices/description/)
```html
-A = [1, 2, 3, 4]
-return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself.
+A = [1, 2, 3, 4]
+return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself.
```
-dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。
+dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。
-在 A[i] - A[i - 1] == A[i - 1] - A[i - 2] 的条件下,{A[i - 2], A[i - 1], A[i]} 是一个等差递增子区间。如果 {A[i - 3], A[i - 2], A[i - 1]} 是一个等差递增子区间,那么 {A[i - 3], A[i - 2], A[i - 1], A[i]} 也是等差递增子区间,dp[i] = dp[i-1] + 1。
+在 A[i] - A[i - 1] == A[i - 1] - A[i - 2] 的条件下,{A[i - 2], A[i - 1], A[i]} 是一个等差递增子区间。如果 {A[i - 3], A[i - 2], A[i - 1]} 是一个等差递增子区间,那么 {A[i - 3], A[i - 2], A[i - 1], A[i]} 也是等差递增子区间,dp[i] = dp[i-1] + 1。
```java
-public int numberOfArithmeticSlices(int[] A) {
- if (A == null || A.length == 0) {
- return 0;
- }
- int n = A.length;
- int[] dp = new int[n];
- for (int i = 2; i < n; i++) {
- if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
- dp[i] = dp[i - 1] + 1;
- }
- }
- int total = 0;
- for (int cnt : dp) {
- total += cnt;
- }
- return total;
+public int numberOfArithmeticSlices(int[] A) {
+ if (A == null || A.length == 0) {
+ return 0;
+ }
+ int n = A.length;
+ int[] dp = new int[n];
+ for (int i = 2; i < n; i++) {
+ if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
+ dp[i] = dp[i - 1] + 1;
+ }
+ }
+ int total = 0;
+ for (int cnt : dp) {
+ total += cnt;
+ }
+ return total;
}
```
-# 分割整数
+# 分割整数
-## 分割整数的最大乘积
+## 分割整数的最大乘积
-[343. Integer Break (Medim)](https://leetcode.com/problems/integer-break/description/)
+[343. Integer Break (Medim)](https://leetcode.com/problems/integer-break/description/)
-题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
+题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
```java
-public int integerBreak(int n) {
- int[] dp = new int[n + 1];
- dp[1] = 1;
- for (int i = 2; i <= n; i++) {
- for (int j = 1; j <= i - 1; j++) {
- dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
- }
- }
- return dp[n];
+public int integerBreak(int n) {
+ int[] dp = new int[n + 1];
+ dp[1] = 1;
+ for (int i = 2; i <= n; i++) {
+ for (int j = 1; j <= i - 1; j++) {
+ dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
+ }
+ }
+ return dp[n];
}
```
-## 按平方数来分割整数
+## 按平方数来分割整数
-[279. Perfect Squares(Medium)](https://leetcode.com/problems/perfect-squares/description/)
+[279. Perfect Squares(Medium)](https://leetcode.com/problems/perfect-squares/description/)
-题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
+题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
```java
-public int numSquares(int n) {
- List squareList = generateSquareList(n);
- int[] dp = new int[n + 1];
- for (int i = 1; i <= n; i++) {
- int min = Integer.MAX_VALUE;
- for (int square : squareList) {
- if (square > i) {
- break;
- }
- min = Math.min(min, dp[i - square] + 1);
- }
- dp[i] = min;
- }
- return dp[n];
+public int numSquares(int n) {
+ List squareList = generateSquareList(n);
+ int[] dp = new int[n + 1];
+ for (int i = 1; i <= n; i++) {
+ int min = Integer.MAX_VALUE;
+ for (int square : squareList) {
+ if (square > i) {
+ break;
+ }
+ min = Math.min(min, dp[i - square] + 1);
+ }
+ dp[i] = min;
+ }
+ return dp[n];
}
-private List generateSquareList(int n) {
- List squareList = new ArrayList<>();
- int diff = 3;
- int square = 1;
- while (square <= n) {
- squareList.add(square);
- square += diff;
- diff += 2;
- }
- return squareList;
+private List generateSquareList(int n) {
+ List squareList = new ArrayList<>();
+ int diff = 3;
+ int square = 1;
+ while (square <= n) {
+ squareList.add(square);
+ square += diff;
+ diff += 2;
+ }
+ return squareList;
}
```
-## 分割整数构成字母字符串
+## 分割整数构成字母字符串
-[91. Decode Ways (Medium)](https://leetcode.com/problems/decode-ways/description/)
+[91. Decode Ways (Medium)](https://leetcode.com/problems/decode-ways/description/)
-题目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).
+题目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).
```java
-public int numDecodings(String s) {
- if (s == null || s.length() == 0) {
- return 0;
- }
- int n = s.length();
- int[] dp = new int[n + 1];
- dp[0] = 1;
- dp[1] = s.charAt(0) == '0' ? 0 : 1;
- for (int i = 2; i <= n; i++) {
- int one = Integer.valueOf(s.substring(i - 1, i));
- if (one != 0) {
- dp[i] += dp[i - 1];
- }
- if (s.charAt(i - 2) == '0') {
- continue;
- }
- int two = Integer.valueOf(s.substring(i - 2, i));
- if (two <= 26) {
- dp[i] += dp[i - 2];
- }
- }
- return dp[n];
+public int numDecodings(String s) {
+ if (s == null || s.length() == 0) {
+ return 0;
+ }
+ int n = s.length();
+ int[] dp = new int[n + 1];
+ dp[0] = 1;
+ dp[1] = s.charAt(0) == '0' ? 0 : 1;
+ for (int i = 2; i <= n; i++) {
+ int one = Integer.valueOf(s.substring(i - 1, i));
+ if (one != 0) {
+ dp[i] += dp[i - 1];
+ }
+ if (s.charAt(i - 2) == '0') {
+ continue;
+ }
+ int two = Integer.valueOf(s.substring(i - 2, i));
+ if (two <= 26) {
+ dp[i] += dp[i - 2];
+ }
+ }
+ return dp[n];
}
```
-# 最长递增子序列
+# 最长递增子序列
-已知一个序列 {S1 , S2 ,...,Sn },取出若干数组成新的序列 {Si1 , Si2 ,..., Sim },其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个**子序列**。
+已知一个序列 {S1 , S2 ,...,Sn },取出若干数组成新的序列 {Si1 , Si2 ,..., Sim },其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列** 。
-如果在子序列中,当下标 ix > iy 时,Six > Siy ,称子序列为原序列的一个**递增子序列**。
+如果在子序列中,当下标 ix > iy 时,Six > Siy ,称子序列为原序列的一个 **递增子序列** 。
-定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1 , Si2 ,...,Sim },如果 im < n 并且 Sim < Sn ,此时 {Si1 , Si2 ,..., Sim , Sn } 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。
+定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1 , Si2 ,...,Sim },如果 im < n 并且 Sim < Sn ,此时 {Si1 , Si2 ,..., Sim , Sn } 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。
-因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn } 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即:
+因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn } 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即:
-
+
-
+
-对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。
+对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。
-## 最长递增子序列
+## 最长递增子序列
-[300. Longest Increasing Subsequence (Medium)](https://leetcode.com/problems/longest-increasing-subsequence/description/)
+[300. Longest Increasing Subsequence (Medium)](https://leetcode.com/problems/longest-increasing-subsequence/description/)
```java
-public int lengthOfLIS(int[] nums) {
- int n = nums.length;
- int[] dp = new int[n];
- for (int i = 0; i < n; i++) {
- int max = 1;
- for (int j = 0; j < i; j++) {
- if (nums[i] > nums[j]) {
- max = Math.max(max, dp[j] + 1);
- }
- }
- dp[i] = max;
- }
- return Arrays.stream(dp).max().orElse(0);
+public int lengthOfLIS(int[] nums) {
+ int n = nums.length;
+ int[] dp = new int[n];
+ for (int i = 0; i < n; i++) {
+ int max = 1;
+ for (int j = 0; j < i; j++) {
+ if (nums[i] > nums[j]) {
+ max = Math.max(max, dp[j] + 1);
+ }
+ }
+ dp[i] = max;
+ }
+ return Arrays.stream(dp).max().orElse(0);
}
```
-使用 Stream 求最大值会导致运行时间过长,可以改成以下形式:
+使用 Stream 求最大值会导致运行时间过长,可以改成以下形式:
```java
-int ret = 0;
-for (int i = 0; i < n; i++) {
- ret = Math.max(ret, dp[i]);
+int ret = 0;
+for (int i = 0; i < n; i++) {
+ ret = Math.max(ret, dp[i]);
}
-return ret;
+return ret;
```
-以上解法的时间复杂度为 O(N2 ),可以使用二分查找将时间复杂度降低为 O(NlogN)。
+以上解法的时间复杂度为 O(N2 ),可以使用二分查找将时间复杂度降低为 O(NlogN)。
-定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x,
+定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x,
-- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1;
-- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x。
+- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1;
+- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x。
-例如对于数组 [4,3,6,5],有:
+例如对于数组 [4,3,6,5],有:
```html
-tails len num
-[] 0 4
-[4] 1 3
-[3] 1 6
-[3,6] 2 5
-[3,5] 2 null
+tails len num
+[] 0 4
+[4] 1 3
+[3] 1 6
+[3,6] 2 5
+[3,5] 2 null
```
-可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。
+可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。
```java
-public int lengthOfLIS(int[] nums) {
- int n = nums.length;
- int[] tails = new int[n];
- int len = 0;
- for (int num : nums) {
- int index = binarySearch(tails, len, num);
- tails[index] = num;
- if (index == len) {
- len++;
- }
- }
- return len;
+public int lengthOfLIS(int[] nums) {
+ int n = nums.length;
+ int[] tails = new int[n];
+ int len = 0;
+ for (int num : nums) {
+ int index = binarySearch(tails, len, num);
+ tails[index] = num;
+ if (index == len) {
+ len++;
+ }
+ }
+ return len;
}
-private int binarySearch(int[] tails, int len, int key) {
- int l = 0, h = len;
- while (l < h) {
- int mid = l + (h - l) / 2;
- if (tails[mid] == key) {
- return mid;
- } else if (tails[mid] > key) {
- h = mid;
- } else {
- l = mid + 1;
- }
- }
- return l;
+private int binarySearch(int[] tails, int len, int key) {
+ int l = 0, h = len;
+ while (l < h) {
+ int mid = l + (h - l) / 2;
+ if (tails[mid] == key) {
+ return mid;
+ } else if (tails[mid] > key) {
+ h = mid;
+ } else {
+ l = mid + 1;
+ }
+ }
+ return l;
}
```
-## 一组整数对能够构成的最长链
+## 一组整数对能够构成的最长链
-[646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/)
+[646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/)
```html
-Input: [[1,2], [2,3], [3,4]]
-Output: 2
-Explanation: The longest chain is [1,2] -> [3,4]
+Input: [[1,2], [2,3], [3,4]]
+Output: 2
+Explanation: The longest chain is [1,2] -> [3,4]
```
-题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。
+题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。
```java
-public int findLongestChain(int[][] pairs) {
- if (pairs == null || pairs.length == 0) {
- return 0;
- }
- Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));
- int n = pairs.length;
- int[] dp = new int[n];
- Arrays.fill(dp, 1);
- for (int i = 1; i < n; i++) {
- for (int j = 0; j < i; j++) {
- if (pairs[j][1] < pairs[i][0]) {
- dp[i] = Math.max(dp[i], dp[j] + 1);
- }
- }
- }
- return Arrays.stream(dp).max().orElse(0);
+public int findLongestChain(int[][] pairs) {
+ if (pairs == null || pairs.length == 0) {
+ return 0;
+ }
+ Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));
+ int n = pairs.length;
+ int[] dp = new int[n];
+ Arrays.fill(dp, 1);
+ for (int i = 1; i < n; i++) {
+ for (int j = 0; j < i; j++) {
+ if (pairs[j][1] < pairs[i][0]) {
+ dp[i] = Math.max(dp[i], dp[j] + 1);
+ }
+ }
+ }
+ return Arrays.stream(dp).max().orElse(0);
}
```
-## 最长摆动子序列
+## 最长摆动子序列
-[376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/)
+[376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/)
```html
-Input: [1,7,4,9,2,5]
-Output: 6
-The entire sequence is a wiggle sequence.
+Input: [1,7,4,9,2,5]
+Output: 6
+The entire sequence is a wiggle sequence.
-Input: [1,17,5,10,13,15,10,5,16,8]
-Output: 7
-There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].
+Input: [1,17,5,10,13,15,10,5,16,8]
+Output: 7
+There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].
-Input: [1,2,3,4,5,6,7,8,9]
-Output: 2
+Input: [1,2,3,4,5,6,7,8,9]
+Output: 2
```
-要求:使用 O(N) 时间复杂度求解。
+要求:使用 O(N) 时间复杂度求解。
```java
-public int wiggleMaxLength(int[] nums) {
- if (nums == null || nums.length == 0) {
- return 0;
- }
- int up = 1, down = 1;
- for (int i = 1; i < nums.length; i++) {
- if (nums[i] > nums[i - 1]) {
- up = down + 1;
- } else if (nums[i] < nums[i - 1]) {
- down = up + 1;
- }
- }
- return Math.max(up, down);
+public int wiggleMaxLength(int[] nums) {
+ if (nums == null || nums.length == 0) {
+ return 0;
+ }
+ int up = 1, down = 1;
+ for (int i = 1; i < nums.length; i++) {
+ if (nums[i] > nums[i - 1]) {
+ up = down + 1;
+ } else if (nums[i] < nums[i - 1]) {
+ down = up + 1;
+ }
+ }
+ return Math.max(up, down);
}
```
-# 最长公共子序列
+# 最长公共子序列
-对于两个子序列 S1 和 S2,找出它们最长的公共子序列。
+对于两个子序列 S1 和 S2,找出它们最长的公共子序列。
-定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:
+定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:
-- 当 S1i ==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。
-- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。
+- 当 S1i ==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。
+- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。
综上,最长公共子序列的状态转移方程为:
-
+
-
+
-对于长度为 N 的序列 S1 和长度为 M 的序列 S2 ,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。
+对于长度为 N 的序列 S1 和长度为 M 的序列 S2 ,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。
与最长递增子序列相比,最长公共子序列有以下不同点:
-- 针对的是两个序列,求它们的最长公共子序列。
-- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。
-- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
+- 针对的是两个序列,求它们的最长公共子序列。
+- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。
+- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
```java
-public int lengthOfLCS(int[] nums1, int[] nums2) {
- int n1 = nums1.length, n2 = nums2.length;
- int[][] dp = new int[n1 + 1][n2 + 1];
- for (int i = 1; i <= n1; i++) {
- for (int j = 1; j <= n2; j++) {
- if (nums1[i - 1] == nums2[j - 1]) {
- dp[i][j] = dp[i - 1][j - 1] + 1;
- } else {
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
- }
- }
- }
- return dp[n1][n2];
+public int lengthOfLCS(int[] nums1, int[] nums2) {
+ int n1 = nums1.length, n2 = nums2.length;
+ int[][] dp = new int[n1 + 1][n2 + 1];
+ for (int i = 1; i <= n1; i++) {
+ for (int j = 1; j <= n2; j++) {
+ if (nums1[i - 1] == nums2[j - 1]) {
+ dp[i][j] = dp[i - 1][j - 1] + 1;
+ } else {
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
+ }
+ }
+ }
+ return dp[n1][n2];
}
```
-# 0-1 背包
+# 0-1 背包
-有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
+有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
-定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
+定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
-- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
-- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。
+- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
+- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。
-第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
+第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
-
+
-
+
```java
-public int knapsack(int W, int N, int[] weights, int[] values) {
- int[][] dp = new int[N + 1][W + 1];
- for (int i = 1; i <= N; i++) {
- int w = weights[i - 1], v = values[i - 1];
- for (int j = 1; j <= W; j++) {
- if (j >= w) {
- dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);
- } else {
- dp[i][j] = dp[i - 1][j];
- }
- }
- }
- return dp[N][W];
+public int knapsack(int W, int N, int[] weights, int[] values) {
+ int[][] dp = new int[N + 1][W + 1];
+ for (int i = 1; i <= N; i++) {
+ int w = weights[i - 1], v = values[i - 1];
+ for (int j = 1; j <= W; j++) {
+ if (j >= w) {
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);
+ } else {
+ dp[i][j] = dp[i - 1][j];
+ }
+ }
+ }
+ return dp[N][W];
}
```
-## 空间优化
+## 空间优化
-在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,
+在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,
-
+
-
+
-因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
+因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
```java
-public int knapsack(int W, int N, int[] weights, int[] values) {
- int[] dp = new int[W + 1];
- for (int i = 1; i <= N; i++) {
- int w = weights[i - 1], v = values[i - 1];
- for (int j = W; j >= 1; j--) {
- if (j >= w) {
- dp[j] = Math.max(dp[j], dp[j - w] + v);
- }
- }
- }
- return dp[W];
+public int knapsack(int W, int N, int[] weights, int[] values) {
+ int[] dp = new int[W + 1];
+ for (int i = 1; i <= N; i++) {
+ int w = weights[i - 1], v = values[i - 1];
+ for (int j = W; j >= 1; j--) {
+ if (j >= w) {
+ dp[j] = Math.max(dp[j], dp[j - w] + v);
+ }
+ }
+ }
+ return dp[W];
}
```
-## 无法使用贪心算法的解释
+## 无法使用贪心算法的解释
-0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22.
+0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22.
-| id | w | v | v/w |
-| --- | --- | --- | --- |
-| 0 | 1 | 6 | 6 |
-| 1 | 2 | 10 | 5 |
-| 2 | 3 | 12 | 4 |
+| id | w | v | v/w |
+| --- | --- | --- | --- |
+| 0 | 1 | 6 | 6 |
+| 1 | 2 | 10 | 5 |
+| 2 | 3 | 12 | 4 |
-## 变种
+## 变种
-- 完全背包:物品数量为无限个
+- 完全背包:物品数量为无限个
-- 多重背包:物品数量有限制
+- 多重背包:物品数量有限制
-- 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制
+- 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制
-- 其它:物品之间相互约束或者依赖
+- 其它:物品之间相互约束或者依赖
-## 划分数组为和相等的两部分
+## 划分数组为和相等的两部分
-[416. Partition Equal Subset Sum (Medium)](https://leetcode.com/problems/partition-equal-subset-sum/description/)
+[416. Partition Equal Subset Sum (Medium)](https://leetcode.com/problems/partition-equal-subset-sum/description/)
```html
-Input: [1, 5, 11, 5]
+Input: [1, 5, 11, 5]
-Output: true
+Output: true
-Explanation: The array can be partitioned as [1, 5, 5] and [11].
+Explanation: The array can be partitioned as [1, 5, 5] and [11].
```
-可以看成一个背包大小为 sum/2 的 0-1 背包问题。
+可以看成一个背包大小为 sum/2 的 0-1 背包问题。
```java
-public boolean canPartition(int[] nums) {
- int sum = computeArraySum(nums);
- if (sum % 2 != 0) {
- return false;
- }
- int W = sum / 2;
- boolean[] dp = new boolean[W + 1];
- dp[0] = true;
- for (int num : nums) { // 0-1 背包一个物品只能用一次
- for (int i = W; i >= num; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num]
- dp[i] = dp[i] || dp[i - num];
- }
- }
- return dp[W];
+public boolean canPartition(int[] nums) {
+ int sum = computeArraySum(nums);
+ if (sum % 2 != 0) {
+ return false;
+ }
+ int W = sum / 2;
+ boolean[] dp = new boolean[W + 1];
+ dp[0] = true;
+ for (int num : nums) { // 0-1 背包一个物品只能用一次
+ for (int i = W; i >= num; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num]
+ dp[i] = dp[i] || dp[i - num];
+ }
+ }
+ return dp[W];
}
-private int computeArraySum(int[] nums) {
- int sum = 0;
- for (int num : nums) {
- sum += num;
- }
- return sum;
+private int computeArraySum(int[] nums) {
+ int sum = 0;
+ for (int num : nums) {
+ sum += num;
+ }
+ return sum;
}
```
-## 改变一组数的正负号使得它们的和为一给定数
+## 改变一组数的正负号使得它们的和为一给定数
-[494. Target Sum (Medium)](https://leetcode.com/problems/target-sum/description/)
+[494. Target Sum (Medium)](https://leetcode.com/problems/target-sum/description/)
```html
-Input: nums is [1, 1, 1, 1, 1], S is 3.
-Output: 5
+Input: nums is [1, 1, 1, 1, 1], S is 3.
+Output: 5
Explanation:
--1+1+1+1+1 = 3
-+1-1+1+1+1 = 3
-+1+1-1+1+1 = 3
-+1+1+1-1+1 = 3
-+1+1+1+1-1 = 3
+-1+1+1+1+1 = 3
++1-1+1+1+1 = 3
++1+1-1+1+1 = 3
++1+1+1-1+1 = 3
++1+1+1+1-1 = 3
-There are 5 ways to assign symbols to make the sum of nums be target 3.
+There are 5 ways to assign symbols to make the sum of nums be target 3.
```
-该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。
+该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。
-可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:
+可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:
```html
- sum(P) - sum(N) = target
-sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
- 2 * sum(P) = target + sum(nums)
+ sum(P) - sum(N) = target
+sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
+ 2 * sum(P) = target + sum(nums)
```
-因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。
+因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。
```java
-public int findTargetSumWays(int[] nums, int S) {
- int sum = computeArraySum(nums);
- if (sum < S || (sum + S) % 2 == 1) {
- return 0;
- }
- int W = (sum + S) / 2;
- int[] dp = new int[W + 1];
- dp[0] = 1;
- for (int num : nums) {
- for (int i = W; i >= num; i--) {
- dp[i] = dp[i] + dp[i - num];
- }
- }
- return dp[W];
+public int findTargetSumWays(int[] nums, int S) {
+ int sum = computeArraySum(nums);
+ if (sum < S || (sum + S) % 2 == 1) {
+ return 0;
+ }
+ int W = (sum + S) / 2;
+ int[] dp = new int[W + 1];
+ dp[0] = 1;
+ for (int num : nums) {
+ for (int i = W; i >= num; i--) {
+ dp[i] = dp[i] + dp[i - num];
+ }
+ }
+ return dp[W];
}
-private int computeArraySum(int[] nums) {
- int sum = 0;
- for (int num : nums) {
- sum += num;
- }
- return sum;
+private int computeArraySum(int[] nums) {
+ int sum = 0;
+ for (int num : nums) {
+ sum += num;
+ }
+ return sum;
}
```
-DFS 解法:
+DFS 解法:
```java
-public int findTargetSumWays(int[] nums, int S) {
- return findTargetSumWays(nums, 0, S);
+public int findTargetSumWays(int[] nums, int S) {
+ return findTargetSumWays(nums, 0, S);
}
-private int findTargetSumWays(int[] nums, int start, int S) {
- if (start == nums.length) {
- return S == 0 ? 1 : 0;
- }
- return findTargetSumWays(nums, start + 1, S + nums[start])
- + findTargetSumWays(nums, start + 1, S - nums[start]);
+private int findTargetSumWays(int[] nums, int start, int S) {
+ if (start == nums.length) {
+ return S == 0 ? 1 : 0;
+ }
+ return findTargetSumWays(nums, start + 1, S + nums[start])
+ + findTargetSumWays(nums, start + 1, S - nums[start]);
}
```
-## 01 字符构成最多的字符串
+## 01 字符构成最多的字符串
-[474. Ones and Zeroes (Medium)](https://leetcode.com/problems/ones-and-zeroes/description/)
+[474. Ones and Zeroes (Medium)](https://leetcode.com/problems/ones-and-zeroes/description/)
```html
-Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
-Output: 4
+Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
+Output: 4
-Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0"
+Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0"
```
-这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。
+这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。
```java
-public int findMaxForm(String[] strs, int m, int n) {
- if (strs == null || strs.length == 0) {
- return 0;
- }
- int[][] dp = new int[m + 1][n + 1];
- for (String s : strs) { // 每个字符串只能用一次
- int ones = 0, zeros = 0;
- for (char c : s.toCharArray()) {
- if (c == '0') {
- zeros++;
- } else {
- ones++;
- }
- }
- for (int i = m; i >= zeros; i--) {
- for (int j = n; j >= ones; j--) {
- dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1);
- }
- }
- }
- return dp[m][n];
+public int findMaxForm(String[] strs, int m, int n) {
+ if (strs == null || strs.length == 0) {
+ return 0;
+ }
+ int[][] dp = new int[m + 1][n + 1];
+ for (String s : strs) { // 每个字符串只能用一次
+ int ones = 0, zeros = 0;
+ for (char c : s.toCharArray()) {
+ if (c == '0') {
+ zeros++;
+ } else {
+ ones++;
+ }
+ }
+ for (int i = m; i >= zeros; i--) {
+ for (int j = n; j >= ones; j--) {
+ dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1);
+ }
+ }
+ }
+ return dp[m][n];
}
```
-## 找零钱的最少硬币数
+## 找零钱的最少硬币数
-[322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/)
+[322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/)
```html
-Example 1:
-coins = [1, 2, 5], amount = 11
-return 3 (11 = 5 + 5 + 1)
+Example 1:
+coins = [1, 2, 5], amount = 11
+return 3 (11 = 5 + 5 + 1)
-Example 2:
-coins = [2], amount = 3
-return -1.
+Example 2:
+coins = [2], amount = 3
+return -1.
```
题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。
-- 物品:硬币
-- 物品大小:面额
-- 物品价值:数量
+- 物品:硬币
+- 物品大小:面额
+- 物品价值:数量
-因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包中逆序遍历 dp 数组改为正序遍历即可。
+因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包中逆序遍历 dp 数组改为正序遍历即可。
```java
-public int coinChange(int[] coins, int amount) {
- if (amount == 0 || coins == null || coins.length == 0) {
- return 0;
- }
- int[] dp = new int[amount + 1];
- for (int coin : coins) {
- for (int i = coin; i <= amount; i++) { //将逆序遍历改为正序遍历
- if (i == coin) {
- dp[i] = 1;
- } else if (dp[i] == 0 && dp[i - coin] != 0) {
- dp[i] = dp[i - coin] + 1;
- } else if (dp[i - coin] != 0) {
- dp[i] = Math.min(dp[i], dp[i - coin] + 1);
- }
- }
- }
- return dp[amount] == 0 ? -1 : dp[amount];
+public int coinChange(int[] coins, int amount) {
+ if (amount == 0 || coins == null || coins.length == 0) {
+ return 0;
+ }
+ int[] dp = new int[amount + 1];
+ for (int coin : coins) {
+ for (int i = coin; i <= amount; i++) { //将逆序遍历改为正序遍历
+ if (i == coin) {
+ dp[i] = 1;
+ } else if (dp[i] == 0 && dp[i - coin] != 0) {
+ dp[i] = dp[i - coin] + 1;
+ } else if (dp[i - coin] != 0) {
+ dp[i] = Math.min(dp[i], dp[i - coin] + 1);
+ }
+ }
+ }
+ return dp[amount] == 0 ? -1 : dp[amount];
}
```
-## 找零钱的硬币数组合
+## 找零钱的硬币数组合
-[518\. Coin Change 2 (Medium)](https://leetcode.com/problems/coin-change-2/description/)
+[518\. Coin Change 2 (Medium)](https://leetcode.com/problems/coin-change-2/description/)
```text-html-basic
-Input: amount = 5, coins = [1, 2, 5]
-Output: 4
-Explanation: there are four ways to make up the amount:
+Input: amount = 5, coins = [1, 2, 5]
+Output: 4
+Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
```
-完全背包问题,使用 dp 记录可达成目标的组合数目。
+完全背包问题,使用 dp 记录可达成目标的组合数目。
```java
-public int change(int amount, int[] coins) {
- if (amount == 0 || coins == null || coins.length == 0) {
- return 0;
- }
- int[] dp = new int[amount + 1];
- dp[0] = 1;
- for (int coin : coins) {
- for (int i = coin; i <= amount; i++) {
- dp[i] += dp[i - coin];
- }
- }
- return dp[amount];
+public int change(int amount, int[] coins) {
+ if (amount == 0 || coins == null || coins.length == 0) {
+ return 0;
+ }
+ int[] dp = new int[amount + 1];
+ dp[0] = 1;
+ for (int coin : coins) {
+ for (int i = coin; i <= amount; i++) {
+ dp[i] += dp[i - coin];
+ }
+ }
+ return dp[amount];
}
```
-## 字符串按单词列表分割
+## 字符串按单词列表分割
-[139. Word Break (Medium)](https://leetcode.com/problems/word-break/description/)
+[139. Word Break (Medium)](https://leetcode.com/problems/word-break/description/)
```html
-s = "leetcode",
-dict = ["leet", "code"].
-Return true because "leetcode" can be segmented as "leet code".
+s = "leetcode",
+dict = ["leet", "code"].
+Return true because "leetcode" can be segmented as "leet code".
```
-dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。该问题涉及到字典中单词的使用顺序,因此可理解为涉及顺序的完全背包问题。
+dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。该问题涉及到字典中单词的使用顺序,因此可理解为涉及顺序的完全背包问题。
求解顺序的完全背包问题时,对物品的迭代应该放在最里层。
```java
-public boolean wordBreak(String s, List wordDict) {
- int n = s.length();
- boolean[] dp = new boolean[n + 1];
- dp[0] = true;
- for (int i = 1; i <= n; i++) {
- for (String word : wordDict) { // 对物品的迭代应该放在最里层
- int len = word.length();
- if (len <= i && word.equals(s.substring(i - len, i))) {
- dp[i] = dp[i] || dp[i - len];
- }
- }
- }
- return dp[n];
+public boolean wordBreak(String s, List wordDict) {
+ int n = s.length();
+ boolean[] dp = new boolean[n + 1];
+ dp[0] = true;
+ for (int i = 1; i <= n; i++) {
+ for (String word : wordDict) { // 对物品的迭代应该放在最里层
+ int len = word.length();
+ if (len <= i && word.equals(s.substring(i - len, i))) {
+ dp[i] = dp[i] || dp[i - len];
+ }
+ }
+ }
+ return dp[n];
}
```
-## 组合总和
+## 组合总和
-[377. Combination Sum IV (Medium)](https://leetcode.com/problems/combination-sum-iv/description/)
+[377. Combination Sum IV (Medium)](https://leetcode.com/problems/combination-sum-iv/description/)
```html
-nums = [1, 2, 3]
-target = 4
+nums = [1, 2, 3]
+target = 4
-The possible combination ways are:
-(1, 1, 1, 1)
-(1, 1, 2)
-(1, 2, 1)
-(1, 3)
-(2, 1, 1)
-(2, 2)
-(3, 1)
+The possible combination ways are:
+(1, 1, 1, 1)
+(1, 1, 2)
+(1, 2, 1)
+(1, 3)
+(2, 1, 1)
+(2, 2)
+(3, 1)
-Note that different sequences are counted as different combinations.
+Note that different sequences are counted as different combinations.
-Therefore the output is 7.
+Therefore the output is 7.
```
涉及顺序的完全背包。
```java
-public int combinationSum4(int[] nums, int target) {
- if (nums == null || nums.length == 0) {
- return 0;
- }
- int[] maximum = new int[target + 1];
- maximum[0] = 1;
- Arrays.sort(nums);
- for (int i = 1; i <= target; i++) {
- for (int j = 0; j < nums.length && nums[j] <= i; j++) {
- maximum[i] += maximum[i - nums[j]];
- }
- }
- return maximum[target];
+public int combinationSum4(int[] nums, int target) {
+ if (nums == null || nums.length == 0) {
+ return 0;
+ }
+ int[] maximum = new int[target + 1];
+ maximum[0] = 1;
+ Arrays.sort(nums);
+ for (int i = 1; i <= target; i++) {
+ for (int j = 0; j < nums.length && nums[j] <= i; j++) {
+ maximum[i] += maximum[i - nums[j]];
+ }
+ }
+ return maximum[target];
}
```
-# 股票交易
+# 股票交易
-## 需要冷却期的股票交易
+## 需要冷却期的股票交易
-[309. Best Time to Buy and Sell Stock with Cooldown(Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/)
+[309. Best Time to Buy and Sell Stock with Cooldown(Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/)
题目描述:交易之后需要有一天的冷却时间。
-
+
```java
-public int maxProfit(int[] prices) {
- if (prices == null || prices.length == 0) {
- return 0;
- }
- int N = prices.length;
- int[] buy = new int[N];
- int[] s1 = new int[N];
- int[] sell = new int[N];
- int[] s2 = new int[N];
- s1[0] = buy[0] = -prices[0];
- sell[0] = s2[0] = 0;
- for (int i = 1; i < N; i++) {
- buy[i] = s2[i - 1] - prices[i];
- s1[i] = Math.max(buy[i - 1], s1[i - 1]);
- sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i];
- s2[i] = Math.max(s2[i - 1], sell[i - 1]);
- }
- return Math.max(sell[N - 1], s2[N - 1]);
+public int maxProfit(int[] prices) {
+ if (prices == null || prices.length == 0) {
+ return 0;
+ }
+ int N = prices.length;
+ int[] buy = new int[N];
+ int[] s1 = new int[N];
+ int[] sell = new int[N];
+ int[] s2 = new int[N];
+ s1[0] = buy[0] = -prices[0];
+ sell[0] = s2[0] = 0;
+ for (int i = 1; i < N; i++) {
+ buy[i] = s2[i - 1] - prices[i];
+ s1[i] = Math.max(buy[i - 1], s1[i - 1]);
+ sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i];
+ s2[i] = Math.max(s2[i - 1], sell[i - 1]);
+ }
+ return Math.max(sell[N - 1], s2[N - 1]);
}
```
-## 需要交易费用的股票交易
+## 需要交易费用的股票交易
-[714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/)
+[714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/)
```html
-Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
-Output: 8
-Explanation: The maximum profit can be achieved by:
-Buying at prices[0] = 1
-Selling at prices[3] = 8
-Buying at prices[4] = 4
-Selling at prices[5] = 9
-The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
+Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
+Output: 8
+Explanation: The maximum profit can be achieved by:
+Buying at prices[0] = 1
+Selling at prices[3] = 8
+Buying at prices[4] = 4
+Selling at prices[5] = 9
+The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
```
题目描述:每交易一次,都要支付一定的费用。
-
+
```java
-public int maxProfit(int[] prices, int fee) {
- int N = prices.length;
- int[] buy = new int[N];
- int[] s1 = new int[N];
- int[] sell = new int[N];
- int[] s2 = new int[N];
- s1[0] = buy[0] = -prices[0];
- sell[0] = s2[0] = 0;
- for (int i = 1; i < N; i++) {
- buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i];
- s1[i] = Math.max(buy[i - 1], s1[i - 1]);
- sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i];
- s2[i] = Math.max(s2[i - 1], sell[i - 1]);
- }
- return Math.max(sell[N - 1], s2[N - 1]);
+public int maxProfit(int[] prices, int fee) {
+ int N = prices.length;
+ int[] buy = new int[N];
+ int[] s1 = new int[N];
+ int[] sell = new int[N];
+ int[] s2 = new int[N];
+ s1[0] = buy[0] = -prices[0];
+ sell[0] = s2[0] = 0;
+ for (int i = 1; i < N; i++) {
+ buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i];
+ s1[i] = Math.max(buy[i - 1], s1[i - 1]);
+ sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i];
+ s2[i] = Math.max(s2[i - 1], sell[i - 1]);
+ }
+ return Math.max(sell[N - 1], s2[N - 1]);
}
```
-## 只能进行两次的股票交易
+## 只能进行两次的股票交易
-[123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/)
+[123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/)
```java
-public int maxProfit(int[] prices) {
- int firstBuy = Integer.MIN_VALUE, firstSell = 0;
- int secondBuy = Integer.MIN_VALUE, secondSell = 0;
- for (int curPrice : prices) {
- if (firstBuy < -curPrice) {
- firstBuy = -curPrice;
- }
- if (firstSell < firstBuy + curPrice) {
- firstSell = firstBuy + curPrice;
- }
- if (secondBuy < firstSell - curPrice) {
- secondBuy = firstSell - curPrice;
- }
- if (secondSell < secondBuy + curPrice) {
- secondSell = secondBuy + curPrice;
- }
- }
- return secondSell;
+public int maxProfit(int[] prices) {
+ int firstBuy = Integer.MIN_VALUE, firstSell = 0;
+ int secondBuy = Integer.MIN_VALUE, secondSell = 0;
+ for (int curPrice : prices) {
+ if (firstBuy < -curPrice) {
+ firstBuy = -curPrice;
+ }
+ if (firstSell < firstBuy + curPrice) {
+ firstSell = firstBuy + curPrice;
+ }
+ if (secondBuy < firstSell - curPrice) {
+ secondBuy = firstSell - curPrice;
+ }
+ if (secondSell < secondBuy + curPrice) {
+ secondSell = secondBuy + curPrice;
+ }
+ }
+ return secondSell;
}
```
-## 只能进行 k 次的股票交易
+## 只能进行 k 次的股票交易
-[188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/)
+[188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/)
```java
-public int maxProfit(int k, int[] prices) {
- int n = prices.length;
- if (k >= n / 2) { // 这种情况下该问题退化为普通的股票交易问题
- int maxProfit = 0;
- for (int i = 1; i < n; i++) {
- if (prices[i] > prices[i - 1]) {
- maxProfit += prices[i] - prices[i - 1];
- }
- }
- return maxProfit;
- }
- int[][] maxProfit = new int[k + 1][n];
- for (int i = 1; i <= k; i++) {
- int localMax = maxProfit[i - 1][0] - prices[0];
- for (int j = 1; j < n; j++) {
- maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax);
- localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]);
- }
- }
- return maxProfit[k][n - 1];
+public int maxProfit(int k, int[] prices) {
+ int n = prices.length;
+ if (k >= n / 2) { // 这种情况下该问题退化为普通的股票交易问题
+ int maxProfit = 0;
+ for (int i = 1; i < n; i++) {
+ if (prices[i] > prices[i - 1]) {
+ maxProfit += prices[i] - prices[i - 1];
+ }
+ }
+ return maxProfit;
+ }
+ int[][] maxProfit = new int[k + 1][n];
+ for (int i = 1; i <= k; i++) {
+ int localMax = maxProfit[i - 1][0] - prices[0];
+ for (int j = 1; j < n; j++) {
+ maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax);
+ localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]);
+ }
+ }
+ return maxProfit[k][n - 1];
}
```
-# 字符串编辑
+# 字符串编辑
-## 删除两个字符串的字符使它们相等
+## 删除两个字符串的字符使它们相等
-[583. Delete Operation for Two Strings (Medium)](https://leetcode.com/problems/delete-operation-for-two-strings/description/)
+[583. Delete Operation for Two Strings (Medium)](https://leetcode.com/problems/delete-operation-for-two-strings/description/)
```html
-Input: "sea", "eat"
-Output: 2
-Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea".
+Input: "sea", "eat"
+Output: 2
+Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea".
```
可以转换为求两个字符串的最长公共子序列问题。
```java
-public int minDistance(String word1, String word2) {
- int m = word1.length(), n = word2.length();
- int[][] dp = new int[m + 1][n + 1];
- for (int i = 1; i <= m; i++) {
- for (int j = 1; j <= n; j++) {
- if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
- dp[i][j] = dp[i - 1][j - 1] + 1;
- } else {
- dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
- }
- }
- }
- return m + n - 2 * dp[m][n];
+public int minDistance(String word1, String word2) {
+ int m = word1.length(), n = word2.length();
+ int[][] dp = new int[m + 1][n + 1];
+ for (int i = 1; i <= m; i++) {
+ for (int j = 1; j <= n; j++) {
+ if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
+ dp[i][j] = dp[i - 1][j - 1] + 1;
+ } else {
+ dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
+ }
+ }
+ }
+ return m + n - 2 * dp[m][n];
}
```
-## 编辑距离
+## 编辑距离
-[72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/)
+[72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/)
```html
-Example 1:
+Example 1:
-Input: word1 = "horse", word2 = "ros"
-Output: 3
+Input: word1 = "horse", word2 = "ros"
+Output: 3
Explanation:
-horse -> rorse (replace 'h' with 'r')
-rorse -> rose (remove 'r')
-rose -> ros (remove 'e')
-Example 2:
+horse -> rorse (replace 'h' with 'r')
+rorse -> rose (remove 'r')
+rose -> ros (remove 'e')
+Example 2:
-Input: word1 = "intention", word2 = "execution"
-Output: 5
+Input: word1 = "intention", word2 = "execution"
+Output: 5
Explanation:
-intention -> inention (remove 't')
-inention -> enention (replace 'i' with 'e')
-enention -> exention (replace 'n' with 'x')
-exention -> exection (replace 'n' with 'c')
-exection -> execution (insert 'u')
+intention -> inention (remove 't')
+inention -> enention (replace 'i' with 'e')
+enention -> exention (replace 'n' with 'x')
+exention -> exection (replace 'n' with 'c')
+exection -> execution (insert 'u')
```
题目描述:修改一个字符串成为另一个字符串,使得修改次数最少。一次修改操作包括:插入一个字符、删除一个字符、替换一个字符。
```java
-public int minDistance(String word1, String word2) {
- if (word1 == null || word2 == null) {
- return 0;
- }
- int m = word1.length(), n = word2.length();
- int[][] dp = new int[m + 1][n + 1];
- for (int i = 1; i <= m; i++) {
- dp[i][0] = i;
- }
- for (int i = 1; i <= n; i++) {
- dp[0][i] = i;
- }
- for (int i = 1; i <= m; i++) {
- for (int j = 1; j <= n; j++) {
- if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
- dp[i][j] = dp[i - 1][j - 1];
- } else {
- dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1;
- }
- }
- }
- return dp[m][n];
+public int minDistance(String word1, String word2) {
+ if (word1 == null || word2 == null) {
+ return 0;
+ }
+ int m = word1.length(), n = word2.length();
+ int[][] dp = new int[m + 1][n + 1];
+ for (int i = 1; i <= m; i++) {
+ dp[i][0] = i;
+ }
+ for (int i = 1; i <= n; i++) {
+ dp[0][i] = i;
+ }
+ for (int i = 1; i <= m; i++) {
+ for (int j = 1; j <= n; j++) {
+ if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
+ dp[i][j] = dp[i - 1][j - 1];
+ } else {
+ dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1;
+ }
+ }
+ }
+ return dp[m][n];
}
```
-## 复制粘贴字符
+## 复制粘贴字符
-[650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/)
+[650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/)
-题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。
+题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。
```
-Input: 3
-Output: 3
+Input: 3
+Output: 3
Explanation:
-Intitally, we have one character 'A'.
-In step 1, we use Copy All operation.
-In step 2, we use Paste operation to get 'AA'.
-In step 3, we use Paste operation to get 'AAA'.
+Intitally, we have one character 'A'.
+In step 1, we use Copy All operation.
+In step 2, we use Paste operation to get 'AA'.
+In step 3, we use Paste operation to get 'AAA'.
```
```java
-public int minSteps(int n) {
- if (n == 1) return 0;
- for (int i = 2; i <= Math.sqrt(n); i++) {
- if (n % i == 0) return i + minSteps(n / i);
- }
- return n;
+public int minSteps(int n) {
+ if (n == 1) return 0;
+ for (int i = 2; i <= Math.sqrt(n); i++) {
+ if (n % i == 0) return i + minSteps(n / i);
+ }
+ return n;
}
```
```java
-public int minSteps(int n) {
- int[] dp = new int[n + 1];
- int h = (int) Math.sqrt(n);
- for (int i = 2; i <= n; i++) {
- dp[i] = i;
- for (int j = 2; j <= h; j++) {
- if (i % j == 0) {
- dp[i] = dp[j] + dp[i / j];
- break;
- }
- }
- }
- return dp[n];
+public int minSteps(int n) {
+ int[] dp = new int[n + 1];
+ int h = (int) Math.sqrt(n);
+ for (int i = 2; i <= n; i++) {
+ dp[i] = i;
+ for (int j = 2; j <= h; j++) {
+ if (i % j == 0) {
+ dp[i] = dp[j] + dp[i / j];
+ break;
+ }
+ }
+ }
+ return dp[n];
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 双指针.md b/docs/notes/Leetcode 题解 - 双指针.md
index c1ecbd6b..f7554ece 100644
--- a/docs/notes/Leetcode 题解 - 双指针.md
+++ b/docs/notes/Leetcode 题解 - 双指针.md
@@ -1,227 +1,244 @@
+
+* [有序数组的 Two Sum](#有序数组的-two-sum)
+* [两数平方和](#两数平方和)
+* [反转字符串中的元音字符](#反转字符串中的元音字符)
+* [回文字符串](#回文字符串)
+* [归并两个有序数组](#归并两个有序数组)
+* [判断链表是否存在环](#判断链表是否存在环)
+* [最长子序列](#最长子序列)
+
+
+
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。
-# 有序数组的 Two Sum
+# 有序数组的 Two Sum
-[Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/)
+[Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/)
```html
-Input: numbers={2, 7, 11, 15}, target=9
-Output: index1=1, index2=2
+Input: numbers={2, 7, 11, 15}, target=9
+Output: index1=1, index2=2
```
-题目描述:在有序数组中找出两个数,使它们的和为 target。
+题目描述:在有序数组中找出两个数,使它们的和为 target。
使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。
-- 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
-- 如果 sum > target,移动较大的元素,使 sum 变小一些;
-- 如果 sum < target,移动较小的元素,使 sum 变大一些。
+- 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
+- 如果 sum > target,移动较大的元素,使 sum 变小一些;
+- 如果 sum < target,移动较小的元素,使 sum 变大一些。
```java
-public int[] twoSum(int[] numbers, int target) {
- int i = 0, j = numbers.length - 1;
- while (i < j) {
- int sum = numbers[i] + numbers[j];
- if (sum == target) {
- return new int[]{i + 1, j + 1};
- } else if (sum < target) {
- i++;
- } else {
- j--;
- }
- }
- return null;
+public int[] twoSum(int[] numbers, int target) {
+ int i = 0, j = numbers.length - 1;
+ while (i < j) {
+ int sum = numbers[i] + numbers[j];
+ if (sum == target) {
+ return new int[]{i + 1, j + 1};
+ } else if (sum < target) {
+ i++;
+ } else {
+ j--;
+ }
+ }
+ return null;
}
```
-# 两数平方和
+# 两数平方和
-[633. Sum of Square Numbers (Easy)](https://leetcode.com/problems/sum-of-square-numbers/description/)
+[633. Sum of Square Numbers (Easy)](https://leetcode.com/problems/sum-of-square-numbers/description/)
```html
-Input: 5
-Output: True
-Explanation: 1 * 1 + 2 * 2 = 5
+Input: 5
+Output: True
+Explanation: 1 * 1 + 2 * 2 = 5
```
题目描述:判断一个数是否为两个数的平方和。
```java
-public boolean judgeSquareSum(int c) {
- int i = 0, j = (int) Math.sqrt(c);
- while (i <= j) {
- int powSum = i * i + j * j;
- if (powSum == c) {
- return true;
- } else if (powSum > c) {
- j--;
- } else {
- i++;
- }
- }
- return false;
+public boolean judgeSquareSum(int c) {
+ int i = 0, j = (int) Math.sqrt(c);
+ while (i <= j) {
+ int powSum = i * i + j * j;
+ if (powSum == c) {
+ return true;
+ } else if (powSum > c) {
+ j--;
+ } else {
+ i++;
+ }
+ }
+ return false;
}
```
-# 反转字符串中的元音字符
+# 反转字符串中的元音字符
-[345. Reverse Vowels of a String (Easy)](https://leetcode.com/problems/reverse-vowels-of-a-string/description/)
+[345. Reverse Vowels of a String (Easy)](https://leetcode.com/problems/reverse-vowels-of-a-string/description/)
```html
-Given s = "leetcode", return "leotcede".
+Given s = "leetcode", return "leotcede".
```
使用双指针指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。
```java
-private final static HashSet vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'));
+private final static HashSet vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'));
-public String reverseVowels(String s) {
- int i = 0, j = s.length() - 1;
- char[] result = new char[s.length()];
- while (i <= j) {
- char ci = s.charAt(i);
- char cj = s.charAt(j);
- if (!vowels.contains(ci)) {
- result[i++] = ci;
- } else if (!vowels.contains(cj)) {
- result[j--] = cj;
- } else {
- result[i++] = cj;
- result[j--] = ci;
- }
- }
- return new String(result);
+public String reverseVowels(String s) {
+ int i = 0, j = s.length() - 1;
+ char[] result = new char[s.length()];
+ while (i <= j) {
+ char ci = s.charAt(i);
+ char cj = s.charAt(j);
+ if (!vowels.contains(ci)) {
+ result[i++] = ci;
+ } else if (!vowels.contains(cj)) {
+ result[j--] = cj;
+ } else {
+ result[i++] = cj;
+ result[j--] = ci;
+ }
+ }
+ return new String(result);
}
```
-# 回文字符串
+# 回文字符串
-[680. Valid Palindrome II (Easy)](https://leetcode.com/problems/valid-palindrome-ii/description/)
+[680. Valid Palindrome II (Easy)](https://leetcode.com/problems/valid-palindrome-ii/description/)
```html
-Input: "abca"
-Output: True
-Explanation: You could delete the character 'c'.
+Input: "abca"
+Output: True
+Explanation: You could delete the character 'c'.
```
题目描述:可以删除一个字符,判断是否能构成回文字符串。
```java
-public boolean validPalindrome(String s) {
- int i = -1, j = s.length();
- while (++i < --j) {
- if (s.charAt(i) != s.charAt(j)) {
- return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j);
- }
- }
- return true;
+public boolean validPalindrome(String s) {
+ int i = -1, j = s.length();
+ while (++i < --j) {
+ if (s.charAt(i) != s.charAt(j)) {
+ return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j);
+ }
+ }
+ return true;
}
-private boolean isPalindrome(String s, int i, int j) {
- while (i < j) {
- if (s.charAt(i++) != s.charAt(j--)) {
- return false;
- }
- }
- return true;
+private boolean isPalindrome(String s, int i, int j) {
+ while (i < j) {
+ if (s.charAt(i++) != s.charAt(j--)) {
+ return false;
+ }
+ }
+ return true;
}
```
-# 归并两个有序数组
+# 归并两个有序数组
-[88. Merge Sorted Array (Easy)](https://leetcode.com/problems/merge-sorted-array/description/)
+[88. Merge Sorted Array (Easy)](https://leetcode.com/problems/merge-sorted-array/description/)
```html
Input:
-nums1 = [1,2,3,0,0,0], m = 3
-nums2 = [2,5,6], n = 3
+nums1 = [1,2,3,0,0,0], m = 3
+nums2 = [2,5,6], n = 3
-Output: [1,2,2,3,5,6]
+Output: [1,2,2,3,5,6]
```
题目描述:把归并结果存到第一个数组上。
-需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值。
+需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值。
```java
-public void merge(int[] nums1, int m, int[] nums2, int n) {
- int index1 = m - 1, index2 = n - 1;
- int indexMerge = m + n - 1;
- while (index1 >= 0 || index2 >= 0) {
- if (index1 < 0) {
- nums1[indexMerge--] = nums2[index2--];
- } else if (index2 < 0) {
- nums1[indexMerge--] = nums1[index1--];
- } else if (nums1[index1] > nums2[index2]) {
- nums1[indexMerge--] = nums1[index1--];
- } else {
- nums1[indexMerge--] = nums2[index2--];
- }
- }
+public void merge(int[] nums1, int m, int[] nums2, int n) {
+ int index1 = m - 1, index2 = n - 1;
+ int indexMerge = m + n - 1;
+ while (index1 >= 0 || index2 >= 0) {
+ if (index1 < 0) {
+ nums1[indexMerge--] = nums2[index2--];
+ } else if (index2 < 0) {
+ nums1[indexMerge--] = nums1[index1--];
+ } else if (nums1[index1] > nums2[index2]) {
+ nums1[indexMerge--] = nums1[index1--];
+ } else {
+ nums1[indexMerge--] = nums2[index2--];
+ }
+ }
}
```
-# 判断链表是否存在环
+# 判断链表是否存在环
-[141. Linked List Cycle (Easy)](https://leetcode.com/problems/linked-list-cycle/description/)
+[141. Linked List Cycle (Easy)](https://leetcode.com/problems/linked-list-cycle/description/)
使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。
```java
-public boolean hasCycle(ListNode head) {
- if (head == null) {
- return false;
- }
- ListNode l1 = head, l2 = head.next;
- while (l1 != null && l2 != null && l2.next != null) {
- if (l1 == l2) {
- return true;
- }
- l1 = l1.next;
- l2 = l2.next.next;
- }
- return false;
+public boolean hasCycle(ListNode head) {
+ if (head == null) {
+ return false;
+ }
+ ListNode l1 = head, l2 = head.next;
+ while (l1 != null && l2 != null && l2.next != null) {
+ if (l1 == l2) {
+ return true;
+ }
+ l1 = l1.next;
+ l2 = l2.next.next;
+ }
+ return false;
}
```
-# 最长子序列
+# 最长子序列
-[524. Longest Word in Dictionary through Deleting (Medium)](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/)
+[524. Longest Word in Dictionary through Deleting (Medium)](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/)
```
Input:
-s = "abpcplea", d = ["ale","apple","monkey","plea"]
+s = "abpcplea", d = ["ale","apple","monkey","plea"]
Output:
"apple"
```
-题目描述:删除 s 中的一些字符,使得它构成字符串列表 d 中的一个字符串,找出能构成的最长字符串。如果有多个相同长度的结果,返回字典序的最小字符串。
+题目描述:删除 s 中的一些字符,使得它构成字符串列表 d 中的一个字符串,找出能构成的最长字符串。如果有多个相同长度的结果,返回字典序的最小字符串。
```java
-public String findLongestWord(String s, List d) {
- String longestWord = "";
- for (String target : d) {
- int l1 = longestWord.length(), l2 = target.length();
- if (l1 > l2 || (l1 == l2 && longestWord.compareTo(target) < 0)) {
- continue;
- }
- if (isValid(s, target)) {
- longestWord = target;
- }
- }
- return longestWord;
+public String findLongestWord(String s, List d) {
+ String longestWord = "";
+ for (String target : d) {
+ int l1 = longestWord.length(), l2 = target.length();
+ if (l1 > l2 || (l1 == l2 && longestWord.compareTo(target) < 0)) {
+ continue;
+ }
+ if (isValid(s, target)) {
+ longestWord = target;
+ }
+ }
+ return longestWord;
}
-private boolean isValid(String s, String target) {
- int i = 0, j = 0;
- while (i < s.length() && j < target.length()) {
- if (s.charAt(i) == target.charAt(j)) {
- j++;
- }
- i++;
- }
- return j == target.length();
+private boolean isValid(String s, String target) {
+ int i = 0, j = 0;
+ while (i < s.length() && j < target.length()) {
+ if (s.charAt(i) == target.charAt(j)) {
+ j++;
+ }
+ i++;
+ }
+ return j == target.length();
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 哈希表.md b/docs/notes/Leetcode 题解 - 哈希表.md
index 9c74a384..06a6605d 100644
--- a/docs/notes/Leetcode 题解 - 哈希表.md
+++ b/docs/notes/Leetcode 题解 - 哈希表.md
@@ -1,115 +1,129 @@
-哈希表使用 O(N) 空间复杂度存储数据,并且以 O(1) 时间复杂度求解问题。
-
-- Java 中的 **HashSet** 用于存储一个集合,可以查找元素是否在集合中。如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。
-
-- Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中 [Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。
+
+* [1. 数组中两个数的和为给定值](#1-数组中两个数的和为给定值)
+* [2. 判断数组是否含有重复元素](#2-判断数组是否含有重复元素)
+* [3. 最长和谐序列](#3-最长和谐序列)
+* [4. 最长连续序列](#4-最长连续序列)
+
-# 1. 数组中两个数的和为给定值
+哈希表使用 O(N) 空间复杂度存储数据,并且以 O(1) 时间复杂度求解问题。
-[1. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/)
+- Java 中的 **HashSet** 用于存储一个集合,可以查找元素是否在集合中。如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。
-可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(NlogN),空间复杂度为 O(1)。
+- Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中 [Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。
-用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i],如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(N),空间复杂度为 O(N),使用空间来换取时间。
+
+# 1. 数组中两个数的和为给定值
+
+[1. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/)
+
+可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(NlogN),空间复杂度为 O(1)。
+
+用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i],如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(N),空间复杂度为 O(N),使用空间来换取时间。
```java
-public int[] twoSum(int[] nums, int target) {
- HashMap indexForNum = new HashMap<>();
- for (int i = 0; i < nums.length; i++) {
- if (indexForNum.containsKey(target - nums[i])) {
- return new int[]{indexForNum.get(target - nums[i]), i};
- } else {
- indexForNum.put(nums[i], i);
- }
- }
- return null;
+public int[] twoSum(int[] nums, int target) {
+ HashMap indexForNum = new HashMap<>();
+ for (int i = 0; i < nums.length; i++) {
+ if (indexForNum.containsKey(target - nums[i])) {
+ return new int[]{indexForNum.get(target - nums[i]), i};
+ } else {
+ indexForNum.put(nums[i], i);
+ }
+ }
+ return null;
}
```
-# 2. 判断数组是否含有重复元素
+# 2. 判断数组是否含有重复元素
-[217. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/)
+[217. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/)
```java
-public boolean containsDuplicate(int[] nums) {
- Set set = new HashSet<>();
- for (int num : nums) {
- set.add(num);
- }
- return set.size() < nums.length;
+public boolean containsDuplicate(int[] nums) {
+ Set set = new HashSet<>();
+ for (int num : nums) {
+ set.add(num);
+ }
+ return set.size() < nums.length;
}
```
-# 3. 最长和谐序列
+# 3. 最长和谐序列
-[594. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/)
+[594. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/)
```html
-Input: [1,3,2,2,5,2,3,7]
-Output: 5
-Explanation: The longest harmonious subsequence is [3,2,2,2,3].
+Input: [1,3,2,2,5,2,3,7]
+Output: 5
+Explanation: The longest harmonious subsequence is [3,2,2,2,3].
```
-和谐序列中最大数和最小数之差正好为 1,应该注意的是序列的元素不一定是数组的连续元素。
+和谐序列中最大数和最小数之差正好为 1,应该注意的是序列的元素不一定是数组的连续元素。
```java
-public int findLHS(int[] nums) {
- Map countForNum = new HashMap<>();
- for (int num : nums) {
- countForNum.put(num, countForNum.getOrDefault(num, 0) + 1);
- }
- int longest = 0;
- for (int num : countForNum.keySet()) {
- if (countForNum.containsKey(num + 1)) {
- longest = Math.max(longest, countForNum.get(num + 1) + countForNum.get(num));
- }
- }
- return longest;
+public int findLHS(int[] nums) {
+ Map countForNum = new HashMap<>();
+ for (int num : nums) {
+ countForNum.put(num, countForNum.getOrDefault(num, 0) + 1);
+ }
+ int longest = 0;
+ for (int num : countForNum.keySet()) {
+ if (countForNum.containsKey(num + 1)) {
+ longest = Math.max(longest, countForNum.get(num + 1) + countForNum.get(num));
+ }
+ }
+ return longest;
}
```
-# 4. 最长连续序列
+# 4. 最长连续序列
-[128. Longest Consecutive Sequence (Hard)](https://leetcode.com/problems/longest-consecutive-sequence/description/)
+[128. Longest Consecutive Sequence (Hard)](https://leetcode.com/problems/longest-consecutive-sequence/description/)
```html
-Given [100, 4, 200, 1, 3, 2],
-The longest consecutive elements sequence is [1, 2, 3, 4]. Return its length: 4.
+Given [100, 4, 200, 1, 3, 2],
+The longest consecutive elements sequence is [1, 2, 3, 4]. Return its length: 4.
```
-要求以 O(N) 的时间复杂度求解。
+要求以 O(N) 的时间复杂度求解。
```java
-public int longestConsecutive(int[] nums) {
- Map countForNum = new HashMap<>();
- for (int num : nums) {
- countForNum.put(num, 1);
- }
- for (int num : nums) {
- forward(countForNum, num);
- }
- return maxCount(countForNum);
+public int longestConsecutive(int[] nums) {
+ Map countForNum = new HashMap<>();
+ for (int num : nums) {
+ countForNum.put(num, 1);
+ }
+ for (int num : nums) {
+ forward(countForNum, num);
+ }
+ return maxCount(countForNum);
}
-private int forward(Map countForNum, int num) {
- if (!countForNum.containsKey(num)) {
- return 0;
- }
- int cnt = countForNum.get(num);
- if (cnt > 1) {
- return cnt;
- }
- cnt = forward(countForNum, num + 1) + 1;
- countForNum.put(num, cnt);
- return cnt;
+private int forward(Map countForNum, int num) {
+ if (!countForNum.containsKey(num)) {
+ return 0;
+ }
+ int cnt = countForNum.get(num);
+ if (cnt > 1) {
+ return cnt;
+ }
+ cnt = forward(countForNum, num + 1) + 1;
+ countForNum.put(num, cnt);
+ return cnt;
}
-private int maxCount(Map countForNum) {
- int max = 0;
- for (int num : countForNum.keySet()) {
- max = Math.max(max, countForNum.get(num));
- }
- return max;
+private int maxCount(Map countForNum) {
+ int max = 0;
+ for (int num : countForNum.keySet()) {
+ max = Math.max(max, countForNum.get(num));
+ }
+ return max;
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 图.md b/docs/notes/Leetcode 题解 - 图.md
index d7774f14..21bb1f81 100644
--- a/docs/notes/Leetcode 题解 - 图.md
+++ b/docs/notes/Leetcode 题解 - 图.md
@@ -1,78 +1,89 @@
-# 二分图
+
+* [二分图](#二分图)
+ * [判断是否为二分图](#判断是否为二分图)
+* [拓扑排序](#拓扑排序)
+ * [课程安排的合法性](#课程安排的合法性)
+ * [课程安排的顺序](#课程安排的顺序)
+* [并查集](#并查集)
+ * [冗余连接](#冗余连接)
+
+
+
+# 二分图
如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。
-## 判断是否为二分图
+## 判断是否为二分图
-[785. Is Graph Bipartite? (Medium)](https://leetcode.com/problems/is-graph-bipartite/description/)
+[785. Is Graph Bipartite? (Medium)](https://leetcode.com/problems/is-graph-bipartite/description/)
```html
-Input: [[1,3], [0,2], [1,3], [0,2]]
-Output: true
+Input: [[1,3], [0,2], [1,3], [0,2]]
+Output: true
Explanation:
-The graph looks like this:
+The graph looks like this:
0----1
-| |
-| |
+| |
+| |
3----2
-We can divide the vertices into two groups: {0, 2} and {1, 3}.
+We can divide the vertices into two groups: {0, 2} and {1, 3}.
```
```html
-Example 2:
-Input: [[1,2,3], [0,2], [0,1,3], [0,2]]
-Output: false
+Example 2:
+Input: [[1,2,3], [0,2], [0,1,3], [0,2]]
+Output: false
Explanation:
-The graph looks like this:
+The graph looks like this:
0----1
-| \ |
-| \ |
+| \ |
+| \ |
3----2
-We cannot find a way to divide the set of nodes into two independent subsets.
+We cannot find a way to divide the set of nodes into two independent subsets.
```
```java
-public boolean isBipartite(int[][] graph) {
- int[] colors = new int[graph.length];
- Arrays.fill(colors, -1);
- for (int i = 0; i < graph.length; i++) { // 处理图不是连通的情况
- if (colors[i] == -1 && !isBipartite(i, 0, colors, graph)) {
- return false;
- }
- }
- return true;
+public boolean isBipartite(int[][] graph) {
+ int[] colors = new int[graph.length];
+ Arrays.fill(colors, -1);
+ for (int i = 0; i < graph.length; i++) { // 处理图不是连通的情况
+ if (colors[i] == -1 && !isBipartite(i, 0, colors, graph)) {
+ return false;
+ }
+ }
+ return true;
}
-private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] graph) {
- if (colors[curNode] != -1) {
- return colors[curNode] == curColor;
- }
- colors[curNode] = curColor;
- for (int nextNode : graph[curNode]) {
- if (!isBipartite(nextNode, 1 - curColor, colors, graph)) {
- return false;
- }
- }
- return true;
+private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] graph) {
+ if (colors[curNode] != -1) {
+ return colors[curNode] == curColor;
+ }
+ colors[curNode] = curColor;
+ for (int nextNode : graph[curNode]) {
+ if (!isBipartite(nextNode, 1 - curColor, colors, graph)) {
+ return false;
+ }
+ }
+ return true;
}
```
-# 拓扑排序
+# 拓扑排序
常用于在具有先序关系的任务规划中。
-## 课程安排的合法性
+## 课程安排的合法性
-[207. Course Schedule (Medium)](https://leetcode.com/problems/course-schedule/description/)
+[207. Course Schedule (Medium)](https://leetcode.com/problems/course-schedule/description/)
```html
-2, [[1,0]]
-return true
+2, [[1,0]]
+return true
```
```html
-2, [[1,0],[0,1]]
-return false
+2, [[1,0],[0,1]]
+return false
```
题目描述:一个课程可能会先修课程,判断给定的先修课程规定是否合法。
@@ -80,167 +91,173 @@ return false
本题不需要使用拓扑排序,只需要检测有向图是否存在环即可。
```java
-public boolean canFinish(int numCourses, int[][] prerequisites) {
- List[] graphic = new List[numCourses];
- for (int i = 0; i < numCourses; i++) {
- graphic[i] = new ArrayList<>();
- }
- for (int[] pre : prerequisites) {
- graphic[pre[0]].add(pre[1]);
- }
- boolean[] globalMarked = new boolean[numCourses];
- boolean[] localMarked = new boolean[numCourses];
- for (int i = 0; i < numCourses; i++) {
- if (hasCycle(globalMarked, localMarked, graphic, i)) {
- return false;
- }
- }
- return true;
+public boolean canFinish(int numCourses, int[][] prerequisites) {
+ List[] graphic = new List[numCourses];
+ for (int i = 0; i < numCourses; i++) {
+ graphic[i] = new ArrayList<>();
+ }
+ for (int[] pre : prerequisites) {
+ graphic[pre[0]].add(pre[1]);
+ }
+ boolean[] globalMarked = new boolean[numCourses];
+ boolean[] localMarked = new boolean[numCourses];
+ for (int i = 0; i < numCourses; i++) {
+ if (hasCycle(globalMarked, localMarked, graphic, i)) {
+ return false;
+ }
+ }
+ return true;
}
-private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked,
- List[] graphic, int curNode) {
+private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked,
+ List[] graphic, int curNode) {
- if (localMarked[curNode]) {
- return true;
- }
- if (globalMarked[curNode]) {
- return false;
- }
- globalMarked[curNode] = true;
- localMarked[curNode] = true;
- for (int nextNode : graphic[curNode]) {
- if (hasCycle(globalMarked, localMarked, graphic, nextNode)) {
- return true;
- }
- }
- localMarked[curNode] = false;
- return false;
+ if (localMarked[curNode]) {
+ return true;
+ }
+ if (globalMarked[curNode]) {
+ return false;
+ }
+ globalMarked[curNode] = true;
+ localMarked[curNode] = true;
+ for (int nextNode : graphic[curNode]) {
+ if (hasCycle(globalMarked, localMarked, graphic, nextNode)) {
+ return true;
+ }
+ }
+ localMarked[curNode] = false;
+ return false;
}
```
-## 课程安排的顺序
+## 课程安排的顺序
-[210. Course Schedule II (Medium)](https://leetcode.com/problems/course-schedule-ii/description/)
+[210. Course Schedule II (Medium)](https://leetcode.com/problems/course-schedule-ii/description/)
```html
-4, [[1,0],[2,0],[3,1],[3,2]]
-There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is[0,2,1,3].
+4, [[1,0],[2,0],[3,1],[3,2]]
+There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is[0,2,1,3].
```
-使用 DFS 来实现拓扑排序,使用一个栈存储后序遍历结果,这个栈的逆序结果就是拓扑排序结果。
+使用 DFS 来实现拓扑排序,使用一个栈存储后序遍历结果,这个栈的逆序结果就是拓扑排序结果。
-证明:对于任何先序关系:v->w,后序遍历结果可以保证 w 先进入栈中,因此栈的逆序结果中 v 会在 w 之前。
+证明:对于任何先序关系:v->w,后序遍历结果可以保证 w 先进入栈中,因此栈的逆序结果中 v 会在 w 之前。
```java
-public int[] findOrder(int numCourses, int[][] prerequisites) {
- List[] graphic = new List[numCourses];
- for (int i = 0; i < numCourses; i++) {
- graphic[i] = new ArrayList<>();
- }
- for (int[] pre : prerequisites) {
- graphic[pre[0]].add(pre[1]);
- }
- Stack postOrder = new Stack<>();
- boolean[] globalMarked = new boolean[numCourses];
- boolean[] localMarked = new boolean[numCourses];
- for (int i = 0; i < numCourses; i++) {
- if (hasCycle(globalMarked, localMarked, graphic, i, postOrder)) {
- return new int[0];
- }
- }
- int[] orders = new int[numCourses];
- for (int i = numCourses - 1; i >= 0; i--) {
- orders[i] = postOrder.pop();
- }
- return orders;
+public int[] findOrder(int numCourses, int[][] prerequisites) {
+ List[] graphic = new List[numCourses];
+ for (int i = 0; i < numCourses; i++) {
+ graphic[i] = new ArrayList<>();
+ }
+ for (int[] pre : prerequisites) {
+ graphic[pre[0]].add(pre[1]);
+ }
+ Stack postOrder = new Stack<>();
+ boolean[] globalMarked = new boolean[numCourses];
+ boolean[] localMarked = new boolean[numCourses];
+ for (int i = 0; i < numCourses; i++) {
+ if (hasCycle(globalMarked, localMarked, graphic, i, postOrder)) {
+ return new int[0];
+ }
+ }
+ int[] orders = new int[numCourses];
+ for (int i = numCourses - 1; i >= 0; i--) {
+ orders[i] = postOrder.pop();
+ }
+ return orders;
}
-private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List[] graphic,
- int curNode, Stack postOrder) {
+private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List[] graphic,
+ int curNode, Stack postOrder) {
- if (localMarked[curNode]) {
- return true;
- }
- if (globalMarked[curNode]) {
- return false;
- }
- globalMarked[curNode] = true;
- localMarked[curNode] = true;
- for (int nextNode : graphic[curNode]) {
- if (hasCycle(globalMarked, localMarked, graphic, nextNode, postOrder)) {
- return true;
- }
- }
- localMarked[curNode] = false;
- postOrder.push(curNode);
- return false;
+ if (localMarked[curNode]) {
+ return true;
+ }
+ if (globalMarked[curNode]) {
+ return false;
+ }
+ globalMarked[curNode] = true;
+ localMarked[curNode] = true;
+ for (int nextNode : graphic[curNode]) {
+ if (hasCycle(globalMarked, localMarked, graphic, nextNode, postOrder)) {
+ return true;
+ }
+ }
+ localMarked[curNode] = false;
+ postOrder.push(curNode);
+ return false;
}
```
-# 并查集
+# 并查集
并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。
-## 冗余连接
+## 冗余连接
-[684. Redundant Connection (Medium)](https://leetcode.com/problems/redundant-connection/description/)
+[684. Redundant Connection (Medium)](https://leetcode.com/problems/redundant-connection/description/)
```html
-Input: [[1,2], [1,3], [2,3]]
-Output: [2,3]
-Explanation: The given undirected graph will be like this:
- 1
- / \
-2 - 3
+Input: [[1,2], [1,3], [2,3]]
+Output: [2,3]
+Explanation: The given undirected graph will be like this:
+ 1
+ / \
+2 - 3
```
题目描述:有一系列的边连成的图,找出一条边,移除它之后该图能够成为一棵树。
```java
-public int[] findRedundantConnection(int[][] edges) {
- int N = edges.length;
- UF uf = new UF(N);
- for (int[] e : edges) {
- int u = e[0], v = e[1];
- if (uf.connect(u, v)) {
- return e;
- }
- uf.union(u, v);
- }
- return new int[]{-1, -1};
+public int[] findRedundantConnection(int[][] edges) {
+ int N = edges.length;
+ UF uf = new UF(N);
+ for (int[] e : edges) {
+ int u = e[0], v = e[1];
+ if (uf.connect(u, v)) {
+ return e;
+ }
+ uf.union(u, v);
+ }
+ return new int[]{-1, -1};
}
-private class UF {
+private class UF {
- private int[] id;
+ private int[] id;
- UF(int N) {
- id = new int[N + 1];
- for (int i = 0; i < id.length; i++) {
- id[i] = i;
- }
- }
+ UF(int N) {
+ id = new int[N + 1];
+ for (int i = 0; i < id.length; i++) {
+ id[i] = i;
+ }
+ }
- void union(int u, int v) {
- int uID = find(u);
- int vID = find(v);
- if (uID == vID) {
- return;
- }
- for (int i = 0; i < id.length; i++) {
- if (id[i] == uID) {
- id[i] = vID;
- }
- }
- }
+ void union(int u, int v) {
+ int uID = find(u);
+ int vID = find(v);
+ if (uID == vID) {
+ return;
+ }
+ for (int i = 0; i < id.length; i++) {
+ if (id[i] == uID) {
+ id[i] = vID;
+ }
+ }
+ }
- int find(int p) {
- return id[p];
- }
+ int find(int p) {
+ return id[p];
+ }
- boolean connect(int u, int v) {
- return find(u) == find(v);
- }
+ boolean connect(int u, int v) {
+ return find(u) == find(v);
+ }
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 字符串.md b/docs/notes/Leetcode 题解 - 字符串.md
index f01d9a40..ec64b3f6 100644
--- a/docs/notes/Leetcode 题解 - 字符串.md
+++ b/docs/notes/Leetcode 题解 - 字符串.md
@@ -1,212 +1,231 @@
-# 字符串循环移位包含
+
+* [字符串循环移位包含](#字符串循环移位包含)
+* [字符串循环移位](#字符串循环移位)
+* [字符串中单词的翻转](#字符串中单词的翻转)
+* [两个字符串包含的字符是否完全相同](#两个字符串包含的字符是否完全相同)
+* [计算一组字符集合可以组成的回文字符串的最大长度](#计算一组字符集合可以组成的回文字符串的最大长度)
+* [字符串同构](#字符串同构)
+* [回文子字符串个数](#回文子字符串个数)
+* [判断一个整数是否是回文数](#判断一个整数是否是回文数)
+* [统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数](#统计二进制字符串中连续-1-和连续-0-数量相同的子字符串个数)
+
-[编程之美 3.1](#)
+
+# 字符串循环移位包含
+
+[编程之美 3.1](#)
```html
-s1 = AABCD, s2 = CDAA
-Return : true
+s1 = AABCD, s2 = CDAA
+Return : true
```
-给定两个字符串 s1 和 s2,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。
+给定两个字符串 s1 和 s2,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。
-s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。
+s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。
-# 字符串循环移位
+# 字符串循环移位
-[编程之美 2.17](#)
+[编程之美 2.17](#)
```html
-s = "abcd123" k = 3
-Return "123abcd"
+s = "abcd123" k = 3
+Return "123abcd"
```
-将字符串向右循环移动 k 位。
+将字符串向右循环移动 k 位。
-将 abcd123 中的 abcd 和 123 单独翻转,得到 dcba321,然后对整个字符串进行翻转,得到 123abcd。
+将 abcd123 中的 abcd 和 123 单独翻转,得到 dcba321,然后对整个字符串进行翻转,得到 123abcd。
-# 字符串中单词的翻转
+# 字符串中单词的翻转
[程序员代码面试指南](#)
```html
-s = "I am a student"
-Return "student a am I"
+s = "I am a student"
+Return "student a am I"
```
将每个单词翻转,然后将整个字符串翻转。
-# 两个字符串包含的字符是否完全相同
+# 两个字符串包含的字符是否完全相同
-[242. Valid Anagram (Easy)](https://leetcode.com/problems/valid-anagram/description/)
+[242. Valid Anagram (Easy)](https://leetcode.com/problems/valid-anagram/description/)
```html
-s = "anagram", t = "nagaram", return true.
-s = "rat", t = "car", return false.
+s = "anagram", t = "nagaram", return true.
+s = "rat", t = "car", return false.
```
-可以用 HashMap 来映射字符与出现次数,然后比较两个字符串出现的字符数量是否相同。
+可以用 HashMap 来映射字符与出现次数,然后比较两个字符串出现的字符数量是否相同。
-由于本题的字符串只包含 26 个小写字符,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,不再使用 HashMap。
+由于本题的字符串只包含 26 个小写字符,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,不再使用 HashMap。
```java
-public boolean isAnagram(String s, String t) {
- int[] cnts = new int[26];
- for (char c : s.toCharArray()) {
- cnts[c - 'a']++;
- }
- for (char c : t.toCharArray()) {
- cnts[c - 'a']--;
- }
- for (int cnt : cnts) {
- if (cnt != 0) {
- return false;
- }
- }
- return true;
+public boolean isAnagram(String s, String t) {
+ int[] cnts = new int[26];
+ for (char c : s.toCharArray()) {
+ cnts[c - 'a']++;
+ }
+ for (char c : t.toCharArray()) {
+ cnts[c - 'a']--;
+ }
+ for (int cnt : cnts) {
+ if (cnt != 0) {
+ return false;
+ }
+ }
+ return true;
}
```
-# 计算一组字符集合可以组成的回文字符串的最大长度
+# 计算一组字符集合可以组成的回文字符串的最大长度
-[409. Longest Palindrome (Easy)](https://leetcode.com/problems/longest-palindrome/description/)
+[409. Longest Palindrome (Easy)](https://leetcode.com/problems/longest-palindrome/description/)
```html
-Input : "abccccdd"
-Output : 7
-Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7.
+Input : "abccccdd"
+Output : 7
+Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7.
```
-使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。
+使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。
因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。
```java
-public int longestPalindrome(String s) {
- int[] cnts = new int[256];
- for (char c : s.toCharArray()) {
- cnts[c]++;
- }
- int palindrome = 0;
- for (int cnt : cnts) {
- palindrome += (cnt / 2) * 2;
- }
- if (palindrome < s.length()) {
- palindrome++; // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间
- }
- return palindrome;
+public int longestPalindrome(String s) {
+ int[] cnts = new int[256];
+ for (char c : s.toCharArray()) {
+ cnts[c]++;
+ }
+ int palindrome = 0;
+ for (int cnt : cnts) {
+ palindrome += (cnt / 2) * 2;
+ }
+ if (palindrome < s.length()) {
+ palindrome++; // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间
+ }
+ return palindrome;
}
```
-# 字符串同构
+# 字符串同构
-[205. Isomorphic Strings (Easy)](https://leetcode.com/problems/isomorphic-strings/description/)
+[205. Isomorphic Strings (Easy)](https://leetcode.com/problems/isomorphic-strings/description/)
```html
-Given "egg", "add", return true.
-Given "foo", "bar", return false.
-Given "paper", "title", return true.
+Given "egg", "add", return true.
+Given "foo", "bar", return false.
+Given "paper", "title", return true.
```
记录一个字符上次出现的位置,如果两个字符串中的字符上次出现的位置一样,那么就属于同构。
```java
-public boolean isIsomorphic(String s, String t) {
- int[] preIndexOfS = new int[256];
- int[] preIndexOfT = new int[256];
- for (int i = 0; i < s.length(); i++) {
- char sc = s.charAt(i), tc = t.charAt(i);
- if (preIndexOfS[sc] != preIndexOfT[tc]) {
- return false;
- }
- preIndexOfS[sc] = i + 1;
- preIndexOfT[tc] = i + 1;
- }
- return true;
+public boolean isIsomorphic(String s, String t) {
+ int[] preIndexOfS = new int[256];
+ int[] preIndexOfT = new int[256];
+ for (int i = 0; i < s.length(); i++) {
+ char sc = s.charAt(i), tc = t.charAt(i);
+ if (preIndexOfS[sc] != preIndexOfT[tc]) {
+ return false;
+ }
+ preIndexOfS[sc] = i + 1;
+ preIndexOfT[tc] = i + 1;
+ }
+ return true;
}
```
-# 回文子字符串个数
+# 回文子字符串个数
-[647. Palindromic Substrings (Medium)](https://leetcode.com/problems/palindromic-substrings/description/)
+[647. Palindromic Substrings (Medium)](https://leetcode.com/problems/palindromic-substrings/description/)
```html
-Input: "aaa"
-Output: 6
-Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
+Input: "aaa"
+Output: 6
+Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
```
从字符串的某一位开始,尝试着去扩展子字符串。
```java
-private int cnt = 0;
+private int cnt = 0;
-public int countSubstrings(String s) {
- for (int i = 0; i < s.length(); i++) {
- extendSubstrings(s, i, i); // 奇数长度
- extendSubstrings(s, i, i + 1); // 偶数长度
- }
- return cnt;
+public int countSubstrings(String s) {
+ for (int i = 0; i < s.length(); i++) {
+ extendSubstrings(s, i, i); // 奇数长度
+ extendSubstrings(s, i, i + 1); // 偶数长度
+ }
+ return cnt;
}
-private void extendSubstrings(String s, int start, int end) {
- while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) {
- start--;
- end++;
- cnt++;
- }
+private void extendSubstrings(String s, int start, int end) {
+ while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) {
+ start--;
+ end++;
+ cnt++;
+ }
}
```
-# 判断一个整数是否是回文数
+# 判断一个整数是否是回文数
-[9. Palindrome Number (Easy)](https://leetcode.com/problems/palindrome-number/description/)
+[9. Palindrome Number (Easy)](https://leetcode.com/problems/palindrome-number/description/)
要求不能使用额外空间,也就不能将整数转换为字符串进行判断。
将整数分成左右两部分,右边那部分需要转置,然后判断这两部分是否相等。
```java
-public boolean isPalindrome(int x) {
- if (x == 0) {
- return true;
- }
- if (x < 0 || x % 10 == 0) {
- return false;
- }
- int right = 0;
- while (x > right) {
- right = right * 10 + x % 10;
- x /= 10;
- }
- return x == right || x == right / 10;
+public boolean isPalindrome(int x) {
+ if (x == 0) {
+ return true;
+ }
+ if (x < 0 || x % 10 == 0) {
+ return false;
+ }
+ int right = 0;
+ while (x > right) {
+ right = right * 10 + x % 10;
+ x /= 10;
+ }
+ return x == right || x == right / 10;
}
```
-# 统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数
+# 统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数
-[696. Count Binary Substrings (Easy)](https://leetcode.com/problems/count-binary-substrings/description/)
+[696. Count Binary Substrings (Easy)](https://leetcode.com/problems/count-binary-substrings/description/)
```html
-Input: "00110011"
-Output: 6
-Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01".
+Input: "00110011"
+Output: 6
+Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01".
```
```java
-public int countBinarySubstrings(String s) {
- int preLen = 0, curLen = 1, count = 0;
- for (int i = 1; i < s.length(); i++) {
- if (s.charAt(i) == s.charAt(i - 1)) {
- curLen++;
- } else {
- preLen = curLen;
- curLen = 1;
- }
+public int countBinarySubstrings(String s) {
+ int preLen = 0, curLen = 1, count = 0;
+ for (int i = 1; i < s.length(); i++) {
+ if (s.charAt(i) == s.charAt(i - 1)) {
+ curLen++;
+ } else {
+ preLen = curLen;
+ curLen = 1;
+ }
- if (preLen >= curLen) {
- count++;
- }
- }
- return count;
+ if (preLen >= curLen) {
+ count++;
+ }
+ }
+ return count;
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 排序.md b/docs/notes/Leetcode 题解 - 排序.md
index 421c423f..f5443af3 100644
--- a/docs/notes/Leetcode 题解 - 排序.md
+++ b/docs/notes/Leetcode 题解 - 排序.md
@@ -1,134 +1,146 @@
-# 快速选择
+
+* [快速选择](#快速选择)
+* [堆排序](#堆排序)
+ * [Kth Element](#kth-element)
+* [桶排序](#桶排序)
+ * [出现频率最多的 k 个数](#出现频率最多的-k-个数)
+ * [按照字符出现次数对字符串排序](#按照字符出现次数对字符串排序)
+* [荷兰国旗问题](#荷兰国旗问题)
+ * [按颜色进行排序](#按颜色进行排序)
+
-用于求解 **Kth Element** 问题,使用快速排序的 partition() 进行实现。
-需要先打乱数组,否则最坏情况下时间复杂度为 O(N2 )。
+# 快速选择
-# 堆排序
+用于求解 **Kth Element** 问题,使用快速排序的 partition() 进行实现。
-用于求解 **TopK Elements** 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。
+需要先打乱数组,否则最坏情况下时间复杂度为 O(N2 )。
-堆排序也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。
+# 堆排序
-快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。
+用于求解 **TopK Elements** 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。
-可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。
+堆排序也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。
-## Kth Element
+快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。
-[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/)
+可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。
-题目描述:找到第 k 大的元素。
+## Kth Element
-**排序**:时间复杂度 O(NlogN),空间复杂度 O(1)
+[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/)
+
+题目描述:找到第 k 大的元素。
+
+**排序** :时间复杂度 O(NlogN),空间复杂度 O(1)
```java
-public int findKthLargest(int[] nums, int k) {
- Arrays.sort(nums);
- return nums[nums.length - k];
+public int findKthLargest(int[] nums, int k) {
+ Arrays.sort(nums);
+ return nums[nums.length - k];
}
```
-**堆排序**:时间复杂度 O(NlogK),空间复杂度 O(K)。
+**堆排序** :时间复杂度 O(NlogK),空间复杂度 O(K)。
```java
-public int findKthLargest(int[] nums, int k) {
- PriorityQueue pq = new PriorityQueue<>(); // 小顶堆
- for (int val : nums) {
- pq.add(val);
- if (pq.size() > k) // 维护堆的大小为 K
- pq.poll();
- }
- return pq.peek();
+public int findKthLargest(int[] nums, int k) {
+ PriorityQueue pq = new PriorityQueue<>(); // 小顶堆
+ for (int val : nums) {
+ pq.add(val);
+ if (pq.size() > k) // 维护堆的大小为 K
+ pq.poll();
+ }
+ return pq.peek();
}
```
-**快速选择**:时间复杂度 O(N),空间复杂度 O(1)
+**快速选择** :时间复杂度 O(N),空间复杂度 O(1)
```java
-public int findKthLargest(int[] nums, int k) {
- k = nums.length - k;
- int l = 0, h = nums.length - 1;
- while (l < h) {
- int j = partition(nums, l, h);
- if (j == k) {
- break;
- } else if (j < k) {
- l = j + 1;
- } else {
- h = j - 1;
- }
- }
- return nums[k];
+public int findKthLargest(int[] nums, int k) {
+ k = nums.length - k;
+ int l = 0, h = nums.length - 1;
+ while (l < h) {
+ int j = partition(nums, l, h);
+ if (j == k) {
+ break;
+ } else if (j < k) {
+ l = j + 1;
+ } else {
+ h = j - 1;
+ }
+ }
+ return nums[k];
}
-private int partition(int[] a, int l, int h) {
- int i = l, j = h + 1;
- while (true) {
- while (a[++i] < a[l] && i < h) ;
- while (a[--j] > a[l] && j > l) ;
- if (i >= j) {
- break;
- }
- swap(a, i, j);
- }
- swap(a, l, j);
- return j;
+private int partition(int[] a, int l, int h) {
+ int i = l, j = h + 1;
+ while (true) {
+ while (a[++i] < a[l] && i < h) ;
+ while (a[--j] > a[l] && j > l) ;
+ if (i >= j) {
+ break;
+ }
+ swap(a, i, j);
+ }
+ swap(a, l, j);
+ return j;
}
-private void swap(int[] a, int i, int j) {
- int t = a[i];
- a[i] = a[j];
- a[j] = t;
+private void swap(int[] a, int i, int j) {
+ int t = a[i];
+ a[i] = a[j];
+ a[j] = t;
}
```
-# 桶排序
+# 桶排序
-## 出现频率最多的 k 个数
+## 出现频率最多的 k 个数
-[347. Top K Frequent Elements (Medium)](https://leetcode.com/problems/top-k-frequent-elements/description/)
+[347. Top K Frequent Elements (Medium)](https://leetcode.com/problems/top-k-frequent-elements/description/)
```html
-Given [1,1,1,2,2,3] and k = 2, return [1,2].
+Given [1,1,1,2,2,3] and k = 2, return [1,2].
```
-设置若干个桶,每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率,即第 i 个桶中存储的数出现的频率为 i。
+设置若干个桶,每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率,即第 i 个桶中存储的数出现的频率为 i。
-把数都放到桶之后,从后向前遍历桶,最先得到的 k 个数就是出现频率最多的的 k 个数。
+把数都放到桶之后,从后向前遍历桶,最先得到的 k 个数就是出现频率最多的的 k 个数。
```java
-public List topKFrequent(int[] nums, int k) {
- Map frequencyForNum = new HashMap<>();
- for (int num : nums) {
- frequencyForNum.put(num, frequencyForNum.getOrDefault(num, 0) + 1);
- }
- List[] buckets = new ArrayList[nums.length + 1];
- for (int key : frequencyForNum.keySet()) {
- int frequency = frequencyForNum.get(key);
- if (buckets[frequency] == null) {
- buckets[frequency] = new ArrayList<>();
- }
- buckets[frequency].add(key);
- }
- List topK = new ArrayList<>();
- for (int i = buckets.length - 1; i >= 0 && topK.size() < k; i--) {
- if (buckets[i] == null) {
- continue;
- }
- if (buckets[i].size() <= (k - topK.size())) {
- topK.addAll(buckets[i]);
- } else {
- topK.addAll(buckets[i].subList(0, k - topK.size()));
- }
- }
- return topK;
+public List topKFrequent(int[] nums, int k) {
+ Map frequencyForNum = new HashMap<>();
+ for (int num : nums) {
+ frequencyForNum.put(num, frequencyForNum.getOrDefault(num, 0) + 1);
+ }
+ List[] buckets = new ArrayList[nums.length + 1];
+ for (int key : frequencyForNum.keySet()) {
+ int frequency = frequencyForNum.get(key);
+ if (buckets[frequency] == null) {
+ buckets[frequency] = new ArrayList<>();
+ }
+ buckets[frequency].add(key);
+ }
+ List topK = new ArrayList<>();
+ for (int i = buckets.length - 1; i >= 0 && topK.size() < k; i--) {
+ if (buckets[i] == null) {
+ continue;
+ }
+ if (buckets[i].size() <= (k - topK.size())) {
+ topK.addAll(buckets[i]);
+ } else {
+ topK.addAll(buckets[i].subList(0, k - topK.size()));
+ }
+ }
+ return topK;
}
```
-## 按照字符出现次数对字符串排序
+## 按照字符出现次数对字符串排序
-[451. Sort Characters By Frequency (Medium)](https://leetcode.com/problems/sort-characters-by-frequency/description/)
+[451. Sort Characters By Frequency (Medium)](https://leetcode.com/problems/sort-characters-by-frequency/description/)
```html
Input:
@@ -138,40 +150,40 @@ Output:
"eert"
Explanation:
-'e' appears twice while 'r' and 't' both appear once.
-So 'e' must appear before both 'r' and 't'. Therefore "eetr" is also a valid answer.
+'e' appears twice while 'r' and 't' both appear once.
+So 'e' must appear before both 'r' and 't'. Therefore "eetr" is also a valid answer.
```
```java
-public String frequencySort(String s) {
- Map frequencyForNum = new HashMap<>();
- for (char c : s.toCharArray())
- frequencyForNum.put(c, frequencyForNum.getOrDefault(c, 0) + 1);
+public String frequencySort(String s) {
+ Map frequencyForNum = new HashMap<>();
+ for (char c : s.toCharArray())
+ frequencyForNum.put(c, frequencyForNum.getOrDefault(c, 0) + 1);
- List[] frequencyBucket = new ArrayList[s.length() + 1];
- for (char c : frequencyForNum.keySet()) {
- int f = frequencyForNum.get(c);
- if (frequencyBucket[f] == null) {
- frequencyBucket[f] = new ArrayList<>();
- }
- frequencyBucket[f].add(c);
- }
- StringBuilder str = new StringBuilder();
- for (int i = frequencyBucket.length - 1; i >= 0; i--) {
- if (frequencyBucket[i] == null) {
- continue;
- }
- for (char c : frequencyBucket[i]) {
- for (int j = 0; j < i; j++) {
- str.append(c);
- }
- }
- }
- return str.toString();
+ List[] frequencyBucket = new ArrayList[s.length() + 1];
+ for (char c : frequencyForNum.keySet()) {
+ int f = frequencyForNum.get(c);
+ if (frequencyBucket[f] == null) {
+ frequencyBucket[f] = new ArrayList<>();
+ }
+ frequencyBucket[f].add(c);
+ }
+ StringBuilder str = new StringBuilder();
+ for (int i = frequencyBucket.length - 1; i >= 0; i--) {
+ if (frequencyBucket[i] == null) {
+ continue;
+ }
+ for (char c : frequencyBucket[i]) {
+ for (int j = 0; j < i; j++) {
+ str.append(c);
+ }
+ }
+ }
+ return str.toString();
}
```
-# 荷兰国旗问题
+# 荷兰国旗问题
荷兰国旗包含三种颜色:红、白、蓝。
@@ -179,36 +191,42 @@ public String frequencySort(String s) {
它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。
-
+
-## 按颜色进行排序
+## 按颜色进行排序
-[75. Sort Colors (Medium)](https://leetcode.com/problems/sort-colors/description/)
+[75. Sort Colors (Medium)](https://leetcode.com/problems/sort-colors/description/)
```html
-Input: [2,0,2,1,1,0]
-Output: [0,0,1,1,2,2]
+Input: [2,0,2,1,1,0]
+Output: [0,0,1,1,2,2]
```
-题目描述:只有 0/1/2 三种颜色。
+题目描述:只有 0/1/2 三种颜色。
```java
-public void sortColors(int[] nums) {
- int zero = -1, one = 0, two = nums.length;
- while (one < two) {
- if (nums[one] == 0) {
- swap(nums, ++zero, one++);
- } else if (nums[one] == 2) {
- swap(nums, --two, one);
- } else {
- ++one;
- }
- }
+public void sortColors(int[] nums) {
+ int zero = -1, one = 0, two = nums.length;
+ while (one < two) {
+ if (nums[one] == 0) {
+ swap(nums, ++zero, one++);
+ } else if (nums[one] == 2) {
+ swap(nums, --two, one);
+ } else {
+ ++one;
+ }
+ }
}
-private void swap(int[] nums, int i, int j) {
- int t = nums[i];
- nums[i] = nums[j];
- nums[j] = t;
+private void swap(int[] nums, int i, int j) {
+ int t = nums[i];
+ nums[i] = nums[j];
+ nums[j] = t;
}
```
+
+
+
+
+⭐️欢迎关注我的公众号 CyC2018,在公众号后台回复关键字 📚 **资料** 可领取复习大纲,这份大纲是我花了一整年时间整理的面试知识点列表,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点。可以说我基本是按照这份大纲来进行复习的,这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助。你们完全可以和我一样根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些复习时间。
+
diff --git a/docs/notes/Leetcode 题解 - 搜索.md b/docs/notes/Leetcode 题解 - 搜索.md
index 315dcca9..1a49792f 100644
--- a/docs/notes/Leetcode 题解 - 搜索.md
+++ b/docs/notes/Leetcode 题解 - 搜索.md
@@ -1,299 +1,329 @@
+
+* [BFS](#bfs)
+ * [计算在网格中从原点到特定点的最短路径长度](#计算在网格中从原点到特定点的最短路径长度)
+ * [组成整数的最小平方数数量](#组成整数的最小平方数数量)
+ * [最短单词路径](#最短单词路径)
+* [DFS](#dfs)
+ * [查找最大的连通面积](#查找最大的连通面积)
+ * [矩阵中的连通分量数目](#矩阵中的连通分量数目)
+ * [好友关系的连通分量数目](#好友关系的连通分量数目)
+ * [填充封闭区域](#填充封闭区域)
+ * [能到达的太平洋和大西洋的区域](#能到达的太平洋和大西洋的区域)
+* [Backtracking](#backtracking)
+ * [数字键盘组合](#数字键盘组合)
+ * [IP 地址划分](#ip-地址划分)
+ * [在矩阵中寻找字符串](#在矩阵中寻找字符串)
+ * [输出二叉树中所有从根到叶子的路径](#输出二叉树中所有从根到叶子的路径)
+ * [排列](#排列)
+ * [含有相同元素求排列](#含有相同元素求排列)
+ * [组合](#组合)
+ * [组合求和](#组合求和)
+ * [含有相同元素的求组合求和](#含有相同元素的求组合求和)
+ * [1-9 数字的组合求和](#1-9-数字的组合求和)
+ * [子集](#子集)
+ * [含有相同元素求子集](#含有相同元素求子集)
+ * [分割字符串使得每个部分都是回文数](#分割字符串使得每个部分都是回文数)
+ * [数独](#数独)
+ * [N 皇后](#n-皇后)
+
+
+
深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。
-# BFS
+# BFS
-
+
广度优先搜索一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
第一层:
-- 0 -> {6,2,1,5}
+- 0 -> {6,2,1,5}
第二层:
-- 6 -> {4}
-- 2 -> {}
-- 1 -> {}
-- 5 -> {3}
+- 6 -> {4}
+- 2 -> {}
+- 1 -> {}
+- 5 -> {3}
第三层:
-- 4 -> {}
-- 3 -> {}
+- 4 -> {}
+- 3 -> {}
-每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj 。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。
+每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj 。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。
-在程序实现 BFS 时需要考虑以下问题:
+在程序实现 BFS 时需要考虑以下问题:
-- 队列:用来存储每一轮遍历得到的节点;
-- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
+- 队列:用来存储每一轮遍历得到的节点;
+- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
-## 计算在网格中从原点到特定点的最短路径长度
+## 计算在网格中从原点到特定点的最短路径长度
```html
[[1,1,0,1],
- [1,0,1,0],
- [1,1,1,1],
- [1,0,1,1]]
+ [1,0,1,0],
+ [1,1,1,1],
+ [1,0,1,1]]
```
-1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。
+1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。
```java
-public int minPathLength(int[][] grids, int tr, int tc) {
- final int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
- final int m = grids.length, n = grids[0].length;
- Queue> queue = new LinkedList<>();
- queue.add(new Pair<>(0, 0));
- int pathLength = 0;
- while (!queue.isEmpty()) {
- int size = queue.size();
- pathLength++;
- while (size-- > 0) {
- Pair cur = queue.poll();
- int cr = cur.getKey(), cc = cur.getValue();
- grids[cr][cc] = 0; // 标记
- for (int[] d : direction) {
- int nr = cr + d[0], nc = cc + d[1];
- if (nr < 0 || nr >= m || nc < 0 || nc >= n || grids[nr][nc] == 0) {
- continue;
- }
- if (nr == tr && nc == tc) {
- return pathLength;
- }
- queue.add(new Pair<>(nr, nc));
- }
- }
- }
- return -1;
+public int minPathLength(int[][] grids, int tr, int tc) {
+ final int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
+ final int m = grids.length, n = grids[0].length;
+ Queue> queue = new LinkedList<>();
+ queue.add(new Pair<>(0, 0));
+ int pathLength = 0;
+ while (!queue.isEmpty()) {
+ int size = queue.size();
+ pathLength++;
+ while (size-- > 0) {
+ Pair cur = queue.poll();
+ int cr = cur.getKey(), cc = cur.getValue();
+ grids[cr][cc] = 0; // 标记
+ for (int[] d : direction) {
+ int nr = cr + d[0], nc = cc + d[1];
+ if (nr < 0 || nr >= m || nc < 0 || nc >= n || grids[nr][nc] == 0) {
+ continue;
+ }
+ if (nr == tr && nc == tc) {
+ return pathLength;
+ }
+ queue.add(new Pair<>(nr, nc));
+ }
+ }
+ }
+ return -1;
}
```
-## 组成整数的最小平方数数量
+## 组成整数的最小平方数数量
-[279. Perfect Squares (Medium)](https://leetcode.com/problems/perfect-squares/description/)
+[279. Perfect Squares (Medium)](https://leetcode.com/problems/perfect-squares/description/)
```html
-For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
+For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
```
可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。
-要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。
+要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。
本题也可以用动态规划求解,在之后动态规划部分中会再次出现。
```java
-public int numSquares(int n) {
- List squares = generateSquares(n);
- Queue queue = new LinkedList<>();
- boolean[] marked = new boolean[n + 1];
- queue.add(n);
- marked[n] = true;
- int level = 0;
- while (!queue.isEmpty()) {
- int size = queue.size();
- level++;
- while (size-- > 0) {
- int cur = queue.poll();
- for (int s : squares) {
- int next = cur - s;
- if (next < 0) {
- break;
- }
- if (next == 0) {
- return level;
- }
- if (marked[next]) {
- continue;
- }
- marked[next] = true;
- queue.add(next);
- }
- }
- }
- return n;
+public int numSquares(int n) {
+ List squares = generateSquares(n);
+ Queue queue = new LinkedList<>();
+ boolean[] marked = new boolean[n + 1];
+ queue.add(n);
+ marked[n] = true;
+ int level = 0;
+ while (!queue.isEmpty()) {
+ int size = queue.size();
+ level++;
+ while (size-- > 0) {
+ int cur = queue.poll();
+ for (int s : squares) {
+ int next = cur - s;
+ if (next < 0) {
+ break;
+ }
+ if (next == 0) {
+ return level;
+ }
+ if (marked[next]) {
+ continue;
+ }
+ marked[next] = true;
+ queue.add(next);
+ }
+ }
+ }
+ return n;
}
/**
- * 生成小于 n 的平方数序列
- * @return 1,4,9,...
- */
-private List generateSquares(int n) {
- List squares = new ArrayList<>();
- int square = 1;
- int diff = 3;
- while (square <= n) {
- squares.add(square);
- square += diff;
- diff += 2;
- }
- return squares;
+ * 生成小于 n 的平方数序列
+ * @return 1,4,9,...
+ */
+private List generateSquares(int n) {
+ List squares = new ArrayList<>();
+ int square = 1;
+ int diff = 3;
+ while (square <= n) {
+ squares.add(square);
+ square += diff;
+ diff += 2;
+ }
+ return squares;
}
```
-## 最短单词路径
+## 最短单词路径
-[127. Word Ladder (Medium)](https://leetcode.com/problems/word-ladder/description/)
+[127. Word Ladder (Medium)](https://leetcode.com/problems/word-ladder/description/)
```html
Input:
-beginWord = "hit",
-endWord = "cog",
-wordList = ["hot","dot","dog","lot","log","cog"]
+beginWord = "hit",
+endWord = "cog",
+wordList = ["hot","dot","dog","lot","log","cog"]
-Output: 5
+Output: 5
-Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
-return its length 5.
+Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
+return its length 5.
```
```html
Input:
-beginWord = "hit"
-endWord = "cog"
-wordList = ["hot","dot","dog","lot","log"]
+beginWord = "hit"
+endWord = "cog"
+wordList = ["hot","dot","dog","lot","log"]
-Output: 0
+Output: 0
-Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.
+Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.
```
-题目描述:找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。
+题目描述:找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。
```java
-public int ladderLength(String beginWord, String endWord, List wordList) {
- wordList.add(beginWord);
- int N = wordList.size();
- int start = N - 1;
- int end = 0;
- while (end < N && !wordList.get(end).equals(endWord)) {
- end++;
- }
- if (end == N) {
- return 0;
- }
- List[] graphic = buildGraphic(wordList);
- return getShortestPath(graphic, start, end);
+public int ladderLength(String beginWord, String endWord, List wordList) {
+ wordList.add(beginWord);
+ int N = wordList.size();
+ int start = N - 1;
+ int end = 0;
+ while (end < N && !wordList.get(end).equals(endWord)) {
+ end++;
+ }
+ if (end == N) {
+ return 0;
+ }
+ List[] graphic = buildGraphic(wordList);
+ return getShortestPath(graphic, start, end);
}
-private List[] buildGraphic(List wordList) {
- int N = wordList.size();
- List[] graphic = new List[N];
- for (int i = 0; i < N; i++) {
- graphic[i] = new ArrayList<>();
- for (int j = 0; j < N; j++) {
- if (isConnect(wordList.get(i), wordList.get(j))) {
- graphic[i].add(j);
- }
- }
- }
- return graphic;
+private List[] buildGraphic(List wordList) {
+ int N = wordList.size();
+ List[] graphic = new List[N];
+ for (int i = 0; i < N; i++) {
+ graphic[i] = new ArrayList<>();
+ for (int j = 0; j < N; j++) {
+ if (isConnect(wordList.get(i), wordList.get(j))) {
+ graphic[i].add(j);
+ }
+ }
+ }
+ return graphic;
}
-private boolean isConnect(String s1, String s2) {
- int diffCnt = 0;
- for (int i = 0; i < s1.length() && diffCnt <= 1; i++) {
- if (s1.charAt(i) != s2.charAt(i)) {
- diffCnt++;
- }
- }
- return diffCnt == 1;
+private boolean isConnect(String s1, String s2) {
+ int diffCnt = 0;
+ for (int i = 0; i < s1.length() && diffCnt <= 1; i++) {
+ if (s1.charAt(i) != s2.charAt(i)) {
+ diffCnt++;
+ }
+ }
+ return diffCnt == 1;
}
-private int getShortestPath(List[] graphic, int start, int end) {
- Queue queue = new LinkedList<>();
- boolean[] marked = new boolean[graphic.length];
- queue.add(start);
- marked[start] = true;
- int path = 1;
- while (!queue.isEmpty()) {
- int size = queue.size();
- path++;
- while (size-- > 0) {
- int cur = queue.poll();
- for (int next : graphic[cur]) {
- if (next == end) {
- return path;
- }
- if (marked[next]) {
- continue;
- }
- marked[next] = true;
- queue.add(next);
- }
- }
- }
- return 0;
+private int getShortestPath(List[] graphic, int start, int end) {
+ Queue queue = new LinkedList<>();
+ boolean[] marked = new boolean[graphic.length];
+ queue.add(start);
+ marked[start] = true;
+ int path = 1;
+ while (!queue.isEmpty()) {
+ int size = queue.size();
+ path++;
+ while (size-- > 0) {
+ int cur = queue.poll();
+ for (int next : graphic[cur]) {
+ if (next == end) {
+ return path;
+ }
+ if (marked[next]) {
+ continue;
+ }
+ marked[next] = true;
+ queue.add(next);
+ }
+ }
+ }
+ return 0;
}
```
-# DFS
+# DFS
-
+
广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。
-而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。
+而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。
-从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。
+从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。
-在程序实现 DFS 时需要考虑以下问题:
+在程序实现 DFS 时需要考虑以下问题:
-- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
-- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
+- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
+- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
-## 查找最大的连通面积
+## 查找最大的连通面积
-[695. Max Area of Island (Easy)](https://leetcode.com/problems/max-area-of-island/description/)
+[695. Max Area of Island (Easy)](https://leetcode.com/problems/max-area-of-island/description/)
```html
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
- [0,0,0,0,0,0,0,1,1,1,0,0,0],
- [0,1,1,0,1,0,0,0,0,0,0,0,0],
- [0,1,0,0,1,1,0,0,1,0,1,0,0],
- [0,1,0,0,1,1,0,0,1,1,1,0,0],
- [0,0,0,0,0,0,0,0,0,0,1,0,0],
- [0,0,0,0,0,0,0,1,1,1,0,0,0],
- [0,0,0,0,0,0,0,1,1,0,0,0,0]]
+ [0,0,0,0,0,0,0,1,1,1,0,0,0],
+ [0,1,1,0,1,0,0,0,0,0,0,0,0],
+ [0,1,0,0,1,1,0,0,1,0,1,0,0],
+ [0,1,0,0,1,1,0,0,1,1,1,0,0],
+ [0,0,0,0,0,0,0,0,0,0,1,0,0],
+ [0,0,0,0,0,0,0,1,1,1,0,0,0],
+ [0,0,0,0,0,0,0,1,1,0,0,0,0]]
```
```java
-private int m, n;
-private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
+private int m, n;
+private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
-public int maxAreaOfIsland(int[][] grid) {
- if (grid == null || grid.length == 0) {
- return 0;
- }
- m = grid.length;
- n = grid[0].length;
- int maxArea = 0;
- for (int i = 0; i < m; i++) {
- for (int j = 0; j < n; j++) {
- maxArea = Math.max(maxArea, dfs(grid, i, j));
- }
- }
- return maxArea;
+public int maxAreaOfIsland(int[][] grid) {
+ if (grid == null || grid.length == 0) {
+ return 0;
+ }
+ m = grid.length;
+ n = grid[0].length;
+ int maxArea = 0;
+ for (int i = 0; i < m; i++) {
+ for (int j = 0; j < n; j++) {
+ maxArea = Math.max(maxArea, dfs(grid, i, j));
+ }
+ }
+ return maxArea;
}
-private int dfs(int[][] grid, int r, int c) {
- if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 0) {
- return 0;
- }
- grid[r][c] = 0;
- int area = 1;
- for (int[] d : direction) {
- area += dfs(grid, r + d[0], c + d[1]);
- }
- return area;
+private int dfs(int[][] grid, int r, int c) {
+ if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 0) {
+ return 0;
+ }
+ grid[r][c] = 0;
+ int area = 1;
+ for (int[] d : direction) {
+ area += dfs(grid, r + d[0], c + d[1]);
+ }
+ return area;
}
```
-## 矩阵中的连通分量数目
+## 矩阵中的连通分量数目
-[200. Number of Islands (Medium)](https://leetcode.com/problems/number-of-islands/description/)
+[200. Number of Islands (Medium)](https://leetcode.com/problems/number-of-islands/description/)
```html
Input:
@@ -302,937 +332,943 @@ Input:
00100
00011
-Output: 3
+Output: 3
```
可以将矩阵表示看成一张有向图。
```java
-private int m, n;
-private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
+private int m, n;
+private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
-public int numIslands(char[][] grid) {
- if (grid == null || grid.length == 0) {
- return 0;
- }
- m = grid.length;
- n = grid[0].length;
- int islandsNum = 0;
- for (int i = 0; i < m; i++) {
- for (int j = 0; j < n; j++) {
- if (grid[i][j] != '0') {
- dfs(grid, i, j);
- islandsNum++;
- }
- }
- }
- return islandsNum;
+public int numIslands(char[][] grid) {
+ if (grid == null || grid.length == 0) {
+ return 0;
+ }
+ m = grid.length;
+ n = grid[0].length;
+ int islandsNum = 0;
+ for (int i = 0; i < m; i++) {
+ for (int j = 0; j < n; j++) {
+ if (grid[i][j] != '0') {
+ dfs(grid, i, j);
+ islandsNum++;
+ }
+ }
+ }
+ return islandsNum;
}
-private void dfs(char[][] grid, int i, int j) {
- if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') {
- return;
- }
- grid[i][j] = '0';
- for (int[] d : direction) {
- dfs(grid, i + d[0], j + d[1]);
- }
+private void dfs(char[][] grid, int i, int j) {
+ if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') {
+ return;
+ }
+ grid[i][j] = '0';
+ for (int[] d : direction) {
+ dfs(grid, i + d[0], j + d[1]);
+ }
}
```
-## 好友关系的连通分量数目
+## 好友关系的连通分量数目
-[547. Friend Circles (Medium)](https://leetcode.com/problems/friend-circles/description/)
+[547. Friend Circles (Medium)](https://leetcode.com/problems/friend-circles/description/)
```html
Input:
[[1,1,0],
- [1,1,0],
- [0,0,1]]
+ [1,1,0],
+ [0,0,1]]
-Output: 2
+Output: 2
-Explanation:The 0th and 1st students are direct friends, so they are in a friend circle.
-The 2nd student himself is in a friend circle. So return 2.
+Explanation:The 0th and 1st students are direct friends, so they are in a friend circle.
+The 2nd student himself is in a friend circle. So return 2.
```
-题目描述:好友关系可以看成是一个无向图,例如第 0 个人与第 1 个人是好友,那么 M[0][1] 和 M[1][0] 的值都为 1。
+题目描述:好友关系可以看成是一个无向图,例如第 0 个人与第 1 个人是好友,那么 M[0][1] 和 M[1][0] 的值都为 1。
```java
-private int n;
+private int n;
-public int findCircleNum(int[][] M) {
- n = M.length;
- int circleNum = 0;
- boolean[] hasVisited = new boolean[n];
- for (int i = 0; i < n; i++) {
- if (!hasVisited[i]) {
- dfs(M, i, hasVisited);
- circleNum++;
- }
- }
- return circleNum;
+public int findCircleNum(int[][] M) {
+ n = M.length;
+ int circleNum = 0;
+ boolean[] hasVisited = new boolean[n];
+ for (int i = 0; i < n; i++) {
+ if (!hasVisited[i]) {
+ dfs(M, i, hasVisited);
+ circleNum++;
+ }
+ }
+ return circleNum;
}
-private void dfs(int[][] M, int i, boolean[] hasVisited) {
- hasVisited[i] = true;
- for (int k = 0; k < n; k++) {
- if (M[i][k] == 1 && !hasVisited[k]) {
- dfs(M, k, hasVisited);
- }
- }
+private void dfs(int[][] M, int i, boolean[] hasVisited) {
+ hasVisited[i] = true;
+ for (int k = 0; k < n; k++) {
+ if (M[i][k] == 1 && !hasVisited[k]) {
+ dfs(M, k, hasVisited);
+ }
+ }
}
```
-## 填充封闭区域
+## 填充封闭区域
-[130. Surrounded Regions (Medium)](https://leetcode.com/problems/surrounded-regions/description/)
+[130. Surrounded Regions (Medium)](https://leetcode.com/problems/surrounded-regions/description/)
```html
-For example,
-X X X X
-X O O X
-X X O X
-X O X X
+For example,
+X X X X
+X O O X
+X X O X
+X O X X
-After running your function, the board should be:
-X X X X
-X X X X
-X X X X
-X O X X
+After running your function, the board should be:
+X X X X
+X X X X
+X X X X
+X O X X
```
-题目描述:使被 'X' 包围的 'O' 转换为 'X'。
+题目描述:使被 'X' 包围的 'O' 转换为 'X'。
先填充最外侧,剩下的就是里侧了。
```java
-private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
-private int m, n;
+private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
+private int m, n;
-public void solve(char[][] board) {
- if (board == null || board.length == 0) {
- return;
- }
+public void solve(char[][] board) {
+ if (board == null || board.length == 0) {
+ return;
+ }
- m = board.length;
- n = board[0].length;
+ m = board.length;
+ n = board[0].length;
- for (int i = 0; i < m; i++) {
- dfs(board, i, 0);
- dfs(board, i, n - 1);
- }
- for (int i = 0; i < n; i++) {
- dfs(board, 0, i);
- dfs(board, m - 1, i);
- }
+ for (int i = 0; i < m; i++) {
+ dfs(board, i, 0);
+ dfs(board, i, n - 1);
+ }
+ for (int i = 0; i < n; i++) {
+ dfs(board, 0, i);
+ dfs(board, m - 1, i);
+ }
- for (int i = 0; i < m; i++) {
- for (int j = 0; j < n; j++) {
- if (board[i][j] == 'T') {
- board[i][j] = 'O';
- } else if (board[i][j] == 'O') {
- board[i][j] = 'X';
- }
- }
- }
+ for (int i = 0; i < m; i++) {
+ for (int j = 0; j < n; j++) {
+ if (board[i][j] == 'T') {
+ board[i][j] = 'O';
+ } else if (board[i][j] == 'O') {
+ board[i][j] = 'X';
+ }
+ }
+ }
}
-private void dfs(char[][] board, int r, int c) {
- if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') {
- return;
- }
- board[r][c] = 'T';
- for (int[] d : direction) {
- dfs(board, r + d[0], c + d[1]);
- }
+private void dfs(char[][] board, int r, int c) {
+ if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') {
+ return;
+ }
+ board[r][c] = 'T';
+ for (int[] d : direction) {
+ dfs(board, r + d[0], c + d[1]);
+ }
}
```
-## 能到达的太平洋和大西洋的区域
+## 能到达的太平洋和大西洋的区域
-[417. Pacific Atlantic Water Flow (Medium)](https://leetcode.com/problems/pacific-atlantic-water-flow/description/)
+[417. Pacific Atlantic Water Flow (Medium)](https://leetcode.com/problems/pacific-atlantic-water-flow/description/)
```html
-Given the following 5x5 matrix:
+Given the following 5x5 matrix:
- Pacific ~ ~ ~ ~ ~
- ~ 1 2 2 3 (5) *
- ~ 3 2 3 (4) (4) *
- ~ 2 4 (5) 3 1 *
- ~ (6) (7) 1 4 5 *
- ~ (5) 1 1 2 4 *
- * * * * * Atlantic
+ Pacific ~ ~ ~ ~ ~
+ ~ 1 2 2 3 (5) *
+ ~ 3 2 3 (4) (4) *
+ ~ 2 4 (5) 3 1 *
+ ~ (6) (7) 1 4 5 *
+ ~ (5) 1 1 2 4 *
+ * * * * * Atlantic
Return:
-[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix).
+[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix).
```
左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。
```java
-private int m, n;
-private int[][] matrix;
-private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
+private int m, n;
+private int[][] matrix;
+private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
-public List pacificAtlantic(int[][] matrix) {
- List ret = new ArrayList<>();
- if (matrix == null || matrix.length == 0) {
- return ret;
- }
+public List pacificAtlantic(int[][] matrix) {
+ List ret = new ArrayList<>();
+ if (matrix == null || matrix.length == 0) {
+ return ret;
+ }
- m = matrix.length;
- n = matrix[0].length;
- this.matrix = matrix;
- boolean[][] canReachP = new boolean[m][n];
- boolean[][] canReachA = new boolean[m][n];
+ m = matrix.length;
+ n = matrix[0].length;
+ this.matrix = matrix;
+ boolean[][] canReachP = new boolean[m][n];
+ boolean[][] canReachA = new boolean[m][n];
- for (int i = 0; i < m; i++) {
- dfs(i, 0, canReachP);
- dfs(i, n - 1, canReachA);
- }
- for (int i = 0; i < n; i++) {
- dfs(0, i, canReachP);
- dfs(m - 1, i, canReachA);
- }
+ for (int i = 0; i < m; i++) {
+ dfs(i, 0, canReachP);
+ dfs(i, n - 1, canReachA);
+ }
+ for (int i = 0; i < n; i++) {
+ dfs(0, i, canReachP);
+ dfs(m - 1, i, canReachA);
+ }
- for (int i = 0; i < m; i++) {
- for (int j = 0; j < n; j++) {
- if (canReachP[i][j] && canReachA[i][j]) {
- ret.add(new int[]{i, j});
- }
- }
- }
+ for (int i = 0; i < m; i++) {
+ for (int j = 0; j < n; j++) {
+ if (canReachP[i][j] && canReachA[i][j]) {
+ ret.add(new int[]{i, j});
+ }
+ }
+ }
- return ret;
+ return ret;
}
-private void dfs(int r, int c, boolean[][] canReach) {
- if (canReach[r][c]) {
- return;
- }
- canReach[r][c] = true;
- for (int[] d : direction) {
- int nextR = d[0] + r;
- int nextC = d[1] + c;
- if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n
- || matrix[r][c] > matrix[nextR][nextC]) {
+private void dfs(int r, int c, boolean[][] canReach) {
+ if (canReach[r][c]) {
+ return;
+ }
+ canReach[r][c] = true;
+ for (int[] d : direction) {
+ int nextR = d[0] + r;
+ int nextC = d[1] + c;
+ if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n
+ || matrix[r][c] > matrix[nextR][nextC]) {
- continue;
- }
- dfs(nextR, nextC, canReach);
- }
+ continue;
+ }
+ dfs(nextR, nextC, canReach);
+ }
}
```
-# Backtracking
+# Backtracking
-Backtracking(回溯)属于 DFS。
+Backtracking(回溯)属于 DFS。
-- 普通 DFS 主要用在 **可达性问题**,这种问题只需要执行到特点的位置然后返回即可。
-- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
+- 普通 DFS 主要用在 **可达性问题** ,这种问题只需要执行到特点的位置然后返回即可。
+- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
-因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
+因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
-- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
-- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
+- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
+- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
-## 数字键盘组合
+## 数字键盘组合
-[17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/)
+[17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/)
-
+
```html
-Input:Digit string "23"
-Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
+Input:Digit string "23"
+Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
```
```java
-private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
+private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
-public List letterCombinations(String digits) {
- List combinations = new ArrayList<>();
- if (digits == null || digits.length() == 0) {
- return combinations;
- }
- doCombination(new StringBuilder(), combinations, digits);
- return combinations;
+public List letterCombinations(String digits) {
+ List combinations = new ArrayList<>();
+ if (digits == null || digits.length() == 0) {
+ return combinations;
+ }
+ doCombination(new StringBuilder(), combinations, digits);
+ return combinations;
}
-private void doCombination(StringBuilder prefix, List combinations, final String digits) {
- if (prefix.length() == digits.length()) {
- combinations.add(prefix.toString());
- return;
- }
- int curDigits = digits.charAt(prefix.length()) - '0';
- String letters = KEYS[curDigits];
- for (char c : letters.toCharArray()) {
- prefix.append(c); // 添加
- doCombination(prefix, combinations, digits);
- prefix.deleteCharAt(prefix.length() - 1); // 删除
- }
+private void doCombination(StringBuilder prefix, List combinations, final String digits) {
+ if (prefix.length() == digits.length()) {
+ combinations.add(prefix.toString());
+ return;
+ }
+ int curDigits = digits.charAt(prefix.length()) - '0';
+ String letters = KEYS[curDigits];
+ for (char c : letters.toCharArray()) {
+ prefix.append(c); // 添加
+ doCombination(prefix, combinations, digits);
+ prefix.deleteCharAt(prefix.length() - 1); // 删除
+ }
}
```
-## IP 地址划分
+## IP 地址划分
-[93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/)
+[93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/)
```html
-Given "25525511135",
-return ["255.255.11.135", "255.255.111.35"].
+Given "25525511135",
+return ["255.255.11.135", "255.255.111.35"].
```
```java
-public List restoreIpAddresses(String s) {
- List addresses = new ArrayList<>();
- StringBuilder tempAddress = new StringBuilder();
- doRestore(0, tempAddress, addresses, s);
- return addresses;
+public List restoreIpAddresses(String s) {
+ List addresses = new ArrayList<>();
+ StringBuilder tempAddress = new StringBuilder();
+ doRestore(0, tempAddress, addresses, s);
+ return addresses;
}
-private void doRestore(int k, StringBuilder tempAddress, List addresses, String s) {
- if (k == 4 || s.length() == 0) {
- if (k == 4 && s.length() == 0) {
- addresses.add(tempAddress.toString());
- }
- return;
- }
- for (int i = 0; i < s.length() && i <= 2; i++) {
- if (i != 0 && s.charAt(0) == '0') {
- break;
- }
- String part = s.substring(0, i + 1);
- if (Integer.valueOf(part) <= 255) {
- if (tempAddress.length() != 0) {
- part = "." + part;
- }
- tempAddress.append(part);
- doRestore(k + 1, tempAddress, addresses, s.substring(i + 1));
- tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length());
- }
- }
+private void doRestore(int k, StringBuilder tempAddress, List addresses, String s) {
+ if (k == 4 || s.length() == 0) {
+ if (k == 4 && s.length() == 0) {
+ addresses.add(tempAddress.toString());
+ }
+ return;
+ }
+ for (int i = 0; i < s.length() && i <= 2; i++) {
+ if (i != 0 && s.charAt(0) == '0') {
+ break;
+ }
+ String part = s.substring(0, i + 1);
+ if (Integer.valueOf(part) <= 255) {
+ if (tempAddress.length() != 0) {
+ part = "." + part;
+ }
+ tempAddress.append(part);
+ doRestore(k + 1, tempAddress, addresses, s.substring(i + 1));
+ tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length());
+ }
+ }
}
```
-## 在矩阵中寻找字符串
+## 在矩阵中寻找字符串
-[79. Word Search (Medium)](https://leetcode.com/problems/word-search/description/)
+[79. Word Search (Medium)](https://leetcode.com/problems/word-search/description/)
```html
-For example,
-Given board =
+For example,
+Given board =
[
- ['A','B','C','E'],
- ['S','F','C','S'],
- ['A','D','E','E']
+ ['A','B','C','E'],
+ ['S','F','C','S'],
+ ['A','D','E','E']
]
-word = "ABCCED", -> returns true,
-word = "SEE", -> returns true,
-word = "ABCB", -> returns false.
+word = "ABCCED", -> returns true,
+word = "SEE", -> returns true,
+word = "ABCB", -> returns false.
```
```java
-private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
-private int m;
-private int n;
+private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
+private int m;
+private int n;
-public boolean exist(char[][] board, String word) {
- if (word == null || word.length() == 0) {
- return true;
- }
- if (board == null || board.length == 0 || board[0].length == 0) {
- return false;
- }
+public boolean exist(char[][] board, String word) {
+ if (word == null || word.length() == 0) {
+ return true;
+ }
+ if (board == null || board.length == 0 || board[0].length == 0) {
+ return false;
+ }
- m = board.length;
- n = board[0].length;
- boolean[][] hasVisited = new boolean[m][n];
+ m = board.length;
+ n = board[0].length;
+ boolean[][] hasVisited = new boolean[m][n];
- for (int r = 0; r < m; r++) {
- for (int c = 0; c < n; c++) {
- if (backtracking(0, r, c, hasVisited, board, word)) {
- return true;
- }
- }
- }
+ for (int r = 0; r < m; r++) {
+ for (int c = 0; c < n; c++) {
+ if (backtracking(0, r, c, hasVisited, board, word)) {
+ return true;
+ }
+ }
+ }
- return false;
+ return false;
}
-private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) {
- if (curLen == word.length()) {
- return true;
- }
- if (r < 0 || r >= m || c < 0 || c >= n
- || board[r][c] != word.charAt(curLen) || visited[r][c]) {
+private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) {
+ if (curLen == word.length()) {
+ return true;
+ }
+ if (r < 0 || r >= m || c < 0 || c >= n
+ || board[r][c] != word.charAt(curLen) || visited[r][c]) {
- return false;
- }
+ return false;
+ }
- visited[r][c] = true;
+ visited[r][c] = true;
- for (int[] d : direction) {
- if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) {
- return true;
- }
- }
+ for (int[] d : direction) {
+ if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) {
+ return true;
+ }
+ }
- visited[r][c] = false;
+ visited[r][c] = false;
- return false;
+ return false;
}
```
-## 输出二叉树中所有从根到叶子的路径
+## 输出二叉树中所有从根到叶子的路径
-[257. Binary Tree Paths (Easy)](https://leetcode.com/problems/binary-tree-paths/description/)
+[257. Binary Tree Paths (Easy)](https://leetcode.com/problems/binary-tree-paths/description/)
```html
- 1
- / \
-2 3
- \
- 5
+ 1
+ / \
+2 3
+ \
+ 5
```
```html
-["1->2->5", "1->3"]
+["1->2->5", "1->3"]
```
```java
-public List binaryTreePaths(TreeNode root) {
- List paths = new ArrayList<>();
- if (root == null) {
- return paths;
- }
- List values = new ArrayList<>();
- backtracking(root, values, paths);
- return paths;
+public List binaryTreePaths(TreeNode root) {
+ List paths = new ArrayList<>();
+ if (root == null) {
+ return paths;
+ }
+ List values = new ArrayList<>();
+ backtracking(root, values, paths);
+ return paths;
}
-private void backtracking(TreeNode node, List values, List paths) {
- if (node == null) {
- return;
- }
- values.add(node.val);
- if (isLeaf(node)) {
- paths.add(buildPath(values));
- } else {
- backtracking(node.left, values, paths);
- backtracking(node.right, values, paths);
- }
- values.remove(values.size() - 1);
+private void backtracking(TreeNode node, List values, List paths) {
+ if (node == null) {
+ return;
+ }
+ values.add(node.val);
+ if (isLeaf(node)) {
+ paths.add(buildPath(values));
+ } else {
+ backtracking(node.left, values, paths);
+ backtracking(node.right, values, paths);
+ }
+ values.remove(values.size() - 1);
}
-private boolean isLeaf(TreeNode node) {
- return node.left == null && node.right == null;
+private boolean isLeaf(TreeNode node) {
+ return node.left == null && node.right == null;
}
-private String buildPath(List values) {
- StringBuilder str = new StringBuilder();
- for (int i = 0; i < values.size(); i++) {
- str.append(values.get(i));
- if (i != values.size() - 1) {
- str.append("->");
- }
- }
- return str.toString();
+private String buildPath(List values) {
+ StringBuilder str = new StringBuilder();
+ for (int i = 0; i < values.size(); i++) {
+ str.append(values.get(i));
+ if (i != values.size() - 1) {
+ str.append("->");
+ }
+ }
+ return str.toString();
}
```
-## 排列
+## 排列
-[46. Permutations (Medium)](https://leetcode.com/problems/permutations/description/)
+[46. Permutations (Medium)](https://leetcode.com/problems/permutations/description/)
```html
-[1,2,3] have the following permutations:
+[1,2,3] have the following permutations:
[
- [1,2,3],
- [1,3,2],
- [2,1,3],
- [2,3,1],
- [3,1,2],
- [3,2,1]
+ [1,2,3],
+ [1,3,2],
+ [2,1,3],
+ [2,3,1],
+ [3,1,2],
+ [3,2,1]
]
```
```java
-public List> permute(int[] nums) {
- List> permutes = new ArrayList<>();
- List permuteList = new ArrayList<>();
- boolean[] hasVisited = new boolean[nums.length];
- backtracking(permuteList, permutes, hasVisited, nums);
- return permutes;
+public List> permute(int[] nums) {
+ List> permutes = new ArrayList<>();
+ List permuteList = new ArrayList<>();
+ boolean[] hasVisited = new boolean[nums.length];
+ backtracking(permuteList, permutes, hasVisited, nums);
+ return permutes;
}
-private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) {
- if (permuteList.size() == nums.length) {
- permutes.add(new ArrayList<>(permuteList)); // 重新构造一个 List
- return;
- }
- for (int i = 0; i < visited.length; i++) {
- if (visited[i]) {
- continue;
- }
- visited[i] = true;
- permuteList.add(nums[i]);
- backtracking(permuteList, permutes, visited, nums);
- permuteList.remove(permuteList.size() - 1);
- visited[i] = false;
- }
+private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) {
+ if (permuteList.size() == nums.length) {
+ permutes.add(new ArrayList<>(permuteList)); // 重新构造一个 List
+ return;
+ }
+ for (int i = 0; i < visited.length; i++) {
+ if (visited[i]) {
+ continue;
+ }
+ visited[i] = true;
+ permuteList.add(nums[i]);
+ backtracking(permuteList, permutes, visited, nums);
+ permuteList.remove(permuteList.size() - 1);
+ visited[i] = false;
+ }
}
```
-## 含有相同元素求排列
+## 含有相同元素求排列
-[47. Permutations II (Medium)](https://leetcode.com/problems/permutations-ii/description/)
+[47. Permutations II (Medium)](https://leetcode.com/problems/permutations-ii/description/)
```html
-[1,1,2] have the following unique permutations:
-[[1,1,2], [1,2,1], [2,1,1]]
+[1,1,2] have the following unique permutations:
+[[1,1,2], [1,2,1], [2,1,1]]
```
数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。
-在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。
+在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。
```java
-public List> permuteUnique(int[] nums) {
- List> permutes = new ArrayList<>();
- List permuteList = new ArrayList<>();
- Arrays.sort(nums); // 排序
- boolean[] hasVisited = new boolean[nums.length];
- backtracking(permuteList, permutes, hasVisited, nums);
- return permutes;
+public List> permuteUnique(int[] nums) {
+ List> permutes = new ArrayList<>();
+ List permuteList = new ArrayList<>();
+ Arrays.sort(nums); // 排序
+ boolean[] hasVisited = new boolean[nums.length];
+ backtracking(permuteList, permutes, hasVisited, nums);
+ return permutes;
}
-private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) {
- if (permuteList.size() == nums.length) {
- permutes.add(new ArrayList<>(permuteList));
- return;
- }
+private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) {
+ if (permuteList.size() == nums.length) {
+ permutes.add(new ArrayList<>(permuteList));
+ return;
+ }
- for (int i = 0; i < visited.length; i++) {
- if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) {
- continue; // 防止重复
- }
- if (visited[i]){
- continue;
- }
- visited[i] = true;
- permuteList.add(nums[i]);
- backtracking(permuteList, permutes, visited, nums);
- permuteList.remove(permuteList.size() - 1);
- visited[i] = false;
- }
+ for (int i = 0; i < visited.length; i++) {
+ if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) {
+ continue; // 防止重复
+ }
+ if (visited[i]){
+ continue;
+ }
+ visited[i] = true;
+ permuteList.add(nums[i]);
+ backtracking(permuteList, permutes, visited, nums);
+ permuteList.remove(permuteList.size() - 1);
+ visited[i] = false;
+ }
}
```
-## 组合
+## 组合
-[77. Combinations (Medium)](https://leetcode.com/problems/combinations/description/)
+[77. Combinations (Medium)](https://leetcode.com/problems/combinations/description/)
```html
-If n = 4 and k = 2, a solution is:
+If n = 4 and k = 2, a solution is:
[
- [2,4],
- [3,4],
- [2,3],
- [1,2],
- [1,3],
- [1,4],
+ [2,4],
+ [3,4],
+ [2,3],
+ [1,2],
+ [1,3],
+ [1,4],
]
```
```java
-public List> combine(int n, int k) {
- List> combinations = new ArrayList<>();
- List combineList = new ArrayList<>();
- backtracking(combineList, combinations, 1, k, n);
- return combinations;
+public List> combine(int n, int k) {
+ List> combinations = new ArrayList<>();
+ List combineList = new ArrayList<>();
+ backtracking(combineList, combinations, 1, k, n);
+ return combinations;
}
-private void backtracking(List combineList, List> combinations, int start, int k, final int n) {
- if (k == 0) {
- combinations.add(new ArrayList<>(combineList));
- return;
- }
- for (int i = start; i <= n - k + 1; i++) { // 剪枝
- combineList.add(i);
- backtracking(combineList, combinations, i + 1, k - 1, n);
- combineList.remove(combineList.size() - 1);
- }
+private void backtracking(List combineList, List> combinations, int start, int k, final int n) {
+ if (k == 0) {
+ combinations.add(new ArrayList<>(combineList));
+ return;
+ }
+ for (int i = start; i <= n - k + 1; i++) { // 剪枝
+ combineList.add(i);
+ backtracking(combineList, combinations, i + 1, k - 1, n);
+ combineList.remove(combineList.size() - 1);
+ }
}
```
-## 组合求和
+## 组合求和
-[39. Combination Sum (Medium)](https://leetcode.com/problems/combination-sum/description/)
+[39. Combination Sum (Medium)](https://leetcode.com/problems/combination-sum/description/)
```html
-given candidate set [2, 3, 6, 7] and target 7,
-A solution set is:
-[[7],[2, 2, 3]]
+given candidate set [2, 3, 6, 7] and target 7,
+A solution set is:
+[[7],[2, 2, 3]]
```
```java
-public List> combinationSum(int[] candidates, int target) {
- List> combinations = new ArrayList<>();
- backtracking(new ArrayList<>(), combinations, 0, target, candidates);
- return combinations;
+public List> combinationSum(int[] candidates, int target) {
+ List> combinations = new ArrayList<>();
+ backtracking(new ArrayList<>(), combinations, 0, target, candidates);
+ return combinations;
}
-private void backtracking(List tempCombination, List> combinations,
- int start, int target, final int[] candidates) {
+private void backtracking(List tempCombination, List> combinations,
+ int start, int target, final int[] candidates) {
- if (target == 0) {
- combinations.add(new ArrayList<>(tempCombination));
- return;
- }
- for (int i = start; i < candidates.length; i++) {
- if (candidates[i] <= target) {
- tempCombination.add(candidates[i]);
- backtracking(tempCombination, combinations, i, target - candidates[i], candidates);
- tempCombination.remove(tempCombination.size() - 1);
- }
- }
+ if (target == 0) {
+ combinations.add(new ArrayList<>(tempCombination));
+ return;
+ }
+ for (int i = start; i < candidates.length; i++) {
+ if (candidates[i] <= target) {
+ tempCombination.add(candidates[i]);
+ backtracking(tempCombination, combinations, i, target - candidates[i], candidates);
+ tempCombination.remove(tempCombination.size() - 1);
+ }
+ }
}
```
-## 含有相同元素的求组合求和
+## 含有相同元素的求组合求和
-[40. Combination Sum II (Medium)](https://leetcode.com/problems/combination-sum-ii/description/)
+[40. Combination Sum II (Medium)](https://leetcode.com/problems/combination-sum-ii/description/)
```html
-For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8,
-A solution set is:
+For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8,
+A solution set is:
[
- [1, 7],
- [1, 2, 5],
- [2, 6],
- [1, 1, 6]
+ [1, 7],
+ [1, 2, 5],
+ [2, 6],
+ [1, 1, 6]
]
```
```java
-public List> combinationSum2(int[] candidates, int target) {
- List> combinations = new ArrayList<>();
- Arrays.sort(candidates);
- backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates);
- return combinations;
+public List> combinationSum2(int[] candidates, int target) {
+ List> combinations = new ArrayList<>();
+ Arrays.sort(candidates);
+ backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates);
+ return combinations;
}
-private void backtracking(List tempCombination, List> combinations,
- boolean[] hasVisited, int start, int target, final int[] candidates) {
+private void backtracking(List tempCombination, List> combinations,
+ boolean[] hasVisited, int start, int target, final int[] candidates) {
- if (target == 0) {
- combinations.add(new ArrayList<>(tempCombination));
- return;
- }
- for (int i = start; i < candidates.length; i++) {
- if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) {
- continue;
- }
- if (candidates[i] <= target) {
- tempCombination.add(candidates[i]);
- hasVisited[i] = true;
- backtracking(tempCombination, combinations, hasVisited, i + 1, target - candidates[i], candidates);
- hasVisited[i] = false;
- tempCombination.remove(tempCombination.size() - 1);
- }
- }
+ if (target == 0) {
+ combinations.add(new ArrayList<>(tempCombination));
+ return;
+ }
+ for (int i = start; i < candidates.length; i++) {
+ if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) {
+ continue;
+ }
+ if (candidates[i] <= target) {
+ tempCombination.add(candidates[i]);
+ hasVisited[i] = true;
+ backtracking(tempCombination, combinations, hasVisited, i + 1, target - candidates[i], candidates);
+ hasVisited[i] = false;
+ tempCombination.remove(tempCombination.size() - 1);
+ }
+ }
}
```
-## 1-9 数字的组合求和
+## 1-9 数字的组合求和
-[216. Combination Sum III (Medium)](https://leetcode.com/problems/combination-sum-iii/description/)
+[216. Combination Sum III (Medium)](https://leetcode.com/problems/combination-sum-iii/description/)
```html
-Input: k = 3, n = 9
+Input: k = 3, n = 9
Output:
-[[1,2,6], [1,3,5], [2,3,4]]
+[[1,2,6], [1,3,5], [2,3,4]]
```
-从 1-9 数字中选出 k 个数不重复的数,使得它们的和为 n。
+从 1-9 数字中选出 k 个数不重复的数,使得它们的和为 n。
```java
-public List> combinationSum3(int k, int n) {
- List> combinations = new ArrayList<>();
- List path = new ArrayList<>();
- backtracking(k, n, 1, path, combinations);
- return combinations;
+public List> combinationSum3(int k, int n) {
+ List> combinations = new ArrayList<>();
+ List path = new ArrayList<>();
+ backtracking(k, n, 1, path, combinations);
+ return combinations;
}
-private void backtracking(int k, int n, int start,
- List tempCombination, List> combinations) {
+private void backtracking(int k, int n, int start,
+ List tempCombination, List> combinations) {
- if (k == 0 && n == 0) {
- combinations.add(new ArrayList<>(tempCombination));
- return;
- }
- if (k == 0 || n == 0) {
- return;
- }
- for (int i = start; i <= 9; i++) {
- tempCombination.add(i);
- backtracking(k - 1, n - i, i + 1, tempCombination, combinations);
- tempCombination.remove(tempCombination.size() - 1);
- }
+ if (k == 0 && n == 0) {
+ combinations.add(new ArrayList<>(tempCombination));
+ return;
+ }
+ if (k == 0 || n == 0) {
+ return;
+ }
+ for (int i = start; i <= 9; i++) {
+ tempCombination.add(i);
+ backtracking(k - 1, n - i, i + 1, tempCombination, combinations);
+ tempCombination.remove(tempCombination.size() - 1);
+ }
}
```
-## 子集
+## 子集
-[78. Subsets (Medium)](https://leetcode.com/problems/subsets/description/)
+[78. Subsets (Medium)](https://leetcode.com/problems/subsets/description/)
-找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复
+找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复
```java
-public List> subsets(int[] nums) {
- List> subsets = new ArrayList<>();
- List tempSubset = new ArrayList<>();
- for (int size = 0; size <= nums.length; size++) {
- backtracking(0, tempSubset, subsets, size, nums); // 不同的子集大小
- }
- return subsets;
+public List> subsets(int[] nums) {
+ List> subsets = new ArrayList<>();
+ List tempSubset = new ArrayList<>();
+ for (int size = 0; size <= nums.length; size++) {
+ backtracking(0, tempSubset, subsets, size, nums); // 不同的子集大小
+ }
+ return subsets;
}
-private void backtracking(int start, List