diff --git a/Git/git.md b/Git/git.md index 87df160b..ad2c43f6 100644 --- a/Git/git.md +++ b/Git/git.md @@ -3,216 +3,245 @@ Git的本地操作 > 参考文献 > * [git使用教程](https://github.com/geeeeeeeeek/git-recipes/wiki) -一个非常详细的中文版git使用详解 +## 1 git安装 -本地操作主要是指在同一个仓库中实现版本控制,包括版本的提交、回退等基本擦偶作。 +- Linux上通过命令行能够很轻松的部署好git +- Windows上需要安装GitBash,模拟linux命令行 +- 安装完成后需要配置全局信息的命令 -**git安装** +``` +git config --global user.name "Your Name" +git config --global user.email "email@example.com" +``` -- Linux上通过命令行能够很轻松的部署好git - -- Windows上需要安装GitBash,模拟linux命令行 - -- 安装完成后需要配置全局信息的命令 - -\$ git config --global user.name "Your Name" - -\$ git config --global user.email "email@example.com" - -创建版本库 - -- git init +## 2 创建版本库 +``` +git init +``` 生成.git文件,将这个目录初始化为一个版本的仓库。 -- git add readme.txt +``` +git add readme.txt +``` 将文件添加到缓冲区,可以连续多次向缓冲区中添加东西 -- git commit -m "wrote a message" - +``` +git commit -m "wrote a message" +``` 将缓冲区的文件提交到历史记录当中当中,可以一次提交对缓冲区的多次修改。 -**时光穿梭机** +## 3 时光穿梭机 -- it status +``` +git status +``` 用来查看当前仓库的状态。主要有changes not staged for commit状态(工作区有变化没有提交),chages to be committed状态(缓存区有变化等待提价) -- git diff readme.txt +``` +git diff readme.txt +``` 用来对比工作区和历史记录内容的差别。 -**版本回退** - -- git log +## 4 版本回退 +``` +git log +``` 用来查看版本的历史记录 -- git log --pretty=oneline +``` +git log --pretty=oneline +``` 在一行显示历史记录 -- git reset --hard HEAD\^ +``` +git reset --hard HEAD\^ +``` 回退到上一次的历史记录 - -- git reset --hard id +``` +git reset --hard id +``` 回退到指定id的版本 -- git reflog +``` +git reflog +``` 用来查看所有操作的历史记录,可以使用reset命令去定位到任何版本 -**工作区和暂存区概念理解** +## 5 工作区和暂存区 -- 工作区Wording Directory:在电脑中能够看到的目录,是工作区 +### **概念理解** -- 版本库Repository:工作区有一个隐藏的目录.git是版本库 +- 工作区Wording Directory:在电脑中能够看到的目录,是工作区 -- 版本库中有Stage暂存区和master等分支以及一个纸箱master的指针HEAD +- 版本库Repository:工作区有一个隐藏的目录.git是版本库 -**管理修改** +- 版本库中有Stage暂存区和master等分支以及一个纸箱master的指针HEAD -- git跟踪管理的是对文件的修改而并非文件本身 +### **管理修改** -**撤销修改** - -- git checkout --readme.txt +- git跟踪管理的是对文件的修改而并非文件本身 +### **撤销修改** +``` +git checkout --readme.txt +``` 把readme.txt在工作区的修改全部撤销掉。如果缓存区有readme.txt的中间状态,则从缓存区恢复,如果缓存区没有readme.txt的中间状态,则从版本库中恢复。 -- git reset HEAD readme.txt - +``` +git reset HEAD readme.txt +``` 把readmen.txt在缓存区的修改全部撤销掉。但是依然会保留工作区的修改。 -**删除文件** - -- git rm test.txt +### **删除文件** +``` +git rm test.txt +``` 把某个文件从缓存区删掉,然后commit之后会从版本库删掉。如果删错了可以通过checkout从缓存区恢复这个文件。 -Git的远程操作 +## 7 Git的远程操作 远程操作主要是指,在不同的仓库之间进行提交和代码更改。是一个明显的对等的分布式系统。其中本地个仓库与远程仓库,不同的远程仓库之间都可以建立这种关系。这种关系之间的操作主要有pull和push。 -**远程仓库** +### **远程仓库** -- 创建SSH key远程仓库和本地仓库一般是通过ssh通信的,需要ssh通信的加密钥匙。 +创建SSH key远程仓库和本地仓库一般是通过ssh通信的,需要ssh通信的加密钥匙。 id_rsa是私钥,id_rsa.pub是公钥,可以告诉别人。 - +``` ssh-keygen -t rsa -C 'yinkanglong@163.com' +``` -- 登录github,添加公钥内容。建立本地与远程仓库的通信协议 +登录github,添加公钥内容。建立本地与远程仓库的通信协议 -**添加远程仓库** +### **添加远程仓库** -- github的界面操作可以很轻松的创建一个远程仓库。但如果想要直接上传自己本地的完整git库,必须创建一个没有lisence和readme的空库。 - -- git remote add origin git@github.com:michaelliao/learngit.git +github的界面操作可以很轻松的创建一个远程仓库。但如果想要直接上传自己本地的完整git库,必须创建一个没有lisence和readme的空库。 +``` +git remote add origin git@github.com:michaelliao/learngit.git +``` 将本地仓库和远程仓库进行关联。一般都叫远程关联仓库为origin,本地的主干分支一般名为master -- git push -u origin master +``` +git push -u origin master +``` 将本地的主干分支master推送到远程的origin分支上。-u参数实现了两个分支的关联,将远程的git仓库的master和本地的master进行了分支管理,当再次执行时,只需要使用git push origin master命令。 -**从远程仓库克隆** +### **从远程仓库克隆** - git clone git@github.com:michaelliao/gitskills.git -分支管理 +## 8 分支管理 这里主要体现的git的功能的分离,这才是真正的git吧。每一个分支都是一个单独的可以分离的工作单位。每个用户可以建立不同的分支进行工作,最终提交到同一个开发分支上。一个用户可以建立不同的分支实现不同的功能,最终提交到同一个用户分支上。分支的灵活性,可以让我们很容易的实现不同分工的分割。 -**创建分支** - -- git checkout -b dev +### **创建分支** +``` +git checkout -b dev +``` 创建分支,并且将工作目录切换到dev的工作目录上。 -- git branch dev - +``` +git branch dev +``` 使用当前版本分支创建新的分支(即添加一个分支指针)。 - -- git checkout dev - +``` +git checkout dev +``` 将工作区切换到某一个分支指针所指的版本上。 - -- git branch - +``` +git branch +``` 会列出所有的分支目录。 -**分支合并** +### **分支合并** -- git merge dev +``` +git merge dev +``` 将dev分支合并到当前工作目录下的分支上(不一定是产生他的父分支) - -- git branch -d dev - +``` +git branch -d dev +``` 用来删除一个已经无效的分支指正,但是在这个分支指针上左右的操作都会保留。 -**解决冲突** +### **解决冲突** - 当合并过程中出现冲突,会进入长途解决工作区,手动解决后提交,就会退出。 - 关于一点,提交后的分支有必要继续工作吗,都行。完全可以提交后删掉重新创建一个分支,获取主干分支上集成的最新内容,然后继续工作。也可以直接在分支上合并一次主干分支,然后得到最新的内容,继续在原来的分支上进行工作。 -- git log --graph +``` +git log --graph +``` 可以查看分支合并图。 -**分支合并方式** - -- git merge --no-ff -m "merge messge" dev - -\--no-ff +### **分支合并方式** +``` +git merge --no-ff -m "merge messge" dev --no-ff +``` 不适用fastfoward方式进行合并,会产生一个新的版本节点。加上-m参数,是因为这种方式合并过程中会自己提交合并后的版本。 -**分支管理** +### **分支管理** - 团队合作的分支管理组图就是你们要的方式 ![clipboard.png](media/202749bd52950c0c839d73190cd2fd9d.png) -**BUG分支** +### **BUG分支** - 因为总会在自己工作的过程中出现紧急的需要更改的bug,但是现在工作区中的内容还不能提交,所以必须生成现在工作区的一个快照,等处理完紧急的bug之后,再来回复工作区的内容。 -- git stash - +``` +git stash +``` 将当前的工作现场储存起来,等到修复完成bug之后可以再次进行更改。工作现场包括工作区和stage缓存区。然后可以清空工作区和缓存区的内容,新建bug分支进行工作。 -- git stash list - +``` +git stash list +``` 用来查看保存中的工作区。 -- git stash apply stash@{0} - +``` +git stash apply stash@{0} +``` 恢复stash内容,但并不清楚stash列表中的内容。 -- get stash drop - +``` +get stash drop +``` 用来删除stash列表中的内容。(因为已经恢复或者没有必要恢复) -- git stash pop - +``` +git stash pop +``` 可以用来弹出stash列表中的内容。 -**Feature分支** +### **Feature分支** - 当添加一个新的功能的时候,因为是一些实验性的代码,所以要创建一个单独的分支进行处理。 - git checkout -b feature-vulcan 创建一个新的功能分支 - +``` git status git add vulcan.py @@ -224,209 +253,118 @@ git checkout dev git merge vulcan.py git branch -D feature-vulcan 不合并,强制删除 - -多人协作 +``` +## 9 多人协作 通过远程库的push和pull操作实现夺人合作 -**推送分支或分支内容** +### **推送分支或分支内容** - 当从远程库进行克隆的时候,实际上已经将本地master分支和远程的master分支进行乐关联。 -- git remote [-v] - +``` +git remote [-v] +``` 可以显示与远程库关联的信息 -- git push origin master +``` +git push origin master +``` 推送分支,吧分支上的所有本地内容提交到远程库中的相同分支当中。 -- 哪些分支需要推送 +### **哪些分支需要推送** +- mater分支是主分支,需要实时推送 +- dev是开发分支,所有成员都要在上面工作。也需要与远程同步。 +- bug分支只需要在本地修复bug,没有必要推送到远程。 +- feature分支,可以不用推送到远程。单人开发不用,夺人开发要推送到远程。 -mater分支是主分支,需要实时推送 - -dev是开发分支,所有成员都要在上面工作。也需要与远程同步。 - -bug分支只需要在本地修复bug,没有必要推送到远程。 - -feature分支,可以不用推送到远程。单人开发不用,夺人开发要推送到远程。 - -**抓取分支或者分支的内容** - -- git checkout -b dev origin/dev +### **抓取分支或者分支的内容** +``` +git checkout -b dev origin/dev +``` 可以用来抓取远程分支dev,这样会建立一个本地的本地的dev分支与远程的dev分支进行关联,可以直接实现dev分支的控制(push) -- git pull \ \ +``` +git pull \ \ +``` 如果Git push失败,说明,当前的版本不是最新的版本。git pull 可以将远程库中的最新版本拉去到本地。 -- git branch --set-upstream dev origin/\ - +``` +git branch --set-upstream dev origin/\ +``` 这样会制定git的本地分支与远程的分支之间的链接。 -- 如果git pull有冲突,与分支合并中解决冲突的方法一直。 +- 如果git pull有冲突,与分支合并中解决冲突的方法一直。 -**多人协作的协作模式通常是这样:** +### **多人协作的协作模式通常是这样:** -> 1\. 首先,可以试图⽤git push origin branch-name推送自己的修改; +1. 首先,可以试图⽤git push origin branch-name推送自己的修改; +2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并; +3. 如果合并有冲突,则解决冲突,并在本地提交; +4. 没有冲突或者解决掉冲突后,再用git push origin branch-name推送就能成功! +5. 如果git pull提“no tracking information”,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream branch-name origin/branch-name。 -> 2\. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并; +## 10 标签管理 -> 3\. 如果合并有冲突,则解决冲突,并在本地提交; +### **创建标签** -> 4\. 没有冲突或者解决掉冲突后,再用git push origin branch-name推送就能成功! - -> 5\. 如果git pull提“no tracking -> information”,则说明本地分支和远程分支的链接关系没有创建,用命令git branch -> \--set-upstream branch-name origin/branch-name。 - -标签管理 - -**创建标签** - -- git tag v1.0 +``` +git tag v1.0 +``` 对当前版本打标签,为v1.0 -- git tag - +``` +git tag +``` 查看所有已经创建的标签 -- git tag v0.9 6.225 +``` +git tag v0.9 6.225 +``` 对指定id的版本打标签 - -- git show v0.9 +``` +git show v0.9 +``` 查看标签信息 -- git tag -a v0.1 -m "version 0.1 released" 3628164 +``` +git tag -a v0.1 -m "version 0.1 released" 3628164 +``` 创建带有标签说明的标签 -**操作标签** +### **操作标签** -- git tag -d v0.1 +``` +git tag -d v0.1 +``` 删除制定版本的标签 -- git push origin v1.0 +``` +git push origin v1.0 +``` 推送某个标签到远程 -- git push origin --tags +``` +git push origin --tags +``` 一次性推送所有标签。 -- git tag -d v0.9 +``` +git tag -d v0.9 git push origin :refs/tags/v0.9 +``` 从远程删除标签,先在本地删除标签,然后将操作推送到远程。 -使用github - -https://www.bilibili.com/video/av10475153/index_2.html\#page=5 - -这是一个比较啰嗦的讲解github页面的东西,学号英文真的十分重要。 - -**git的简介** - -Create Repository - -创建仓库 - -Star - -收藏 - -Fork - -复制一份远程仓库 - -Pull Request - -拉我呀,快点,我改好了。 - -Open & Merge Request - -拉你了,别墨迹。 - -Watch - -看着你呢,关注一个项目。 - -Github主页 - -仓库主页——仓库相关的信息和相关操作 - -个人主页 - -这是一些补充的内容 - -保持同步 - -**git remote** - -- git remote - -列出你和远程仓库之间的远程连接 - -- git remote -v - -列出每个连接的名字和url - -- git remote add \ \ - -创建一个新的远程连接并添加名字 - -- git remote rm \ - -移除远程仓库的链接 - -git fetch - -- git fetch \ - -拉取仓库中的所有分支(包括相关的文件和所有的提交) - -- git fetch \ \ - -拉取制定仓库中的所有分支(包括相关的文件和所欲的提交) - -\> -注意,这个步骤知识拉取远程的分支,在本地并没有合并也没有生成本地分支,知识一个可读的远程分支。使用 -git branch -r 命令可以查看所有只读的远程分支。使用git -checkout命令可以创建本地分支,并与远程分支关联。使用git merge -命令可以将远程分支与本地分支合并。 - -git pull - -- git pull remote - -拉取当前分支对应的远程副本,并将远程副本的更改写入本地副本。相当于git -fetch之后git merge。 - -- git pull -rebase \ - -使用git rebase命令合并远程分支与本地分支,不使用git merge - -git push - -- git push \ \ - -将制定分支推送到远程分支。包括所有的文件和提交。 - -- git push \ --force - -强制推送 - -- git push \ --all - -本地所有的分支推送到远程仓库当中 - -- git push \ --tags - -将本地所有标签推送到远程仓库中 diff --git a/Git/github.md b/Git/github.md new file mode 100644 index 00000000..114c66e9 --- /dev/null +++ b/Git/github.md @@ -0,0 +1,98 @@ +## 1 使用github +> 参考文献 +> [https://www.bilibili.com/video/av10475153/index_2.html\#page=5] + + +### **github的简介** + +* Create Repository创建仓库 +* Star收藏 +* Fork复制一份远程仓库 +* Pull Request拉我呀,快点,我改好了。 +* Open & Merge Request拉你了,别墨迹。 +* Watch看着你呢,关注一个项目。 +* Github主页仓库主页——仓库相关的信息和相关操作 + + + + + +### **git remote** + +``` +git remote +``` + +列出你和远程仓库之间的远程连接 + +``` +git remote -v +``` + +列出每个连接的名字和url + +``` +git remote add \ \ +``` + +创建一个新的远程连接并添加名字 +``` +git remote rm \ +``` + +移除远程仓库的链接 + +### **git fetch** + +``` +git fetch \ +``` + +拉取仓库中的所有分支(包括相关的文件和所有的提交) +``` +git fetch \ \ +``` + +拉取制定仓库中的所有分支(包括相关的文件和所欲的提交) + +> 注意,这个步骤知识拉取远程的分支,在本地并没有合并也没有生成本地分支,知识一个可读的远程分支。 +> * 使用git branch -r 命令可以查看所有只读的远程分支。 +> * 使用gitcheckout命令可以创建本地分支,并与远程分支关联。 +> * 使用git merge命令可以将远程分支与本地分支合并。 + +### **git pull** +``` +git pull remote +``` + +拉取当前分支对应的远程副本,并将远程副本的更改写入本地副本。相当于git fetch之后git merge。 + +``` +git pull -rebase \ +``` + +使用git rebase命令合并远程分支与本地分支,不使用git merge + +### **git push** + +``` +git push \ \ +``` + +将制定分支推送到远程分支。包括所有的文件和提交。 + +``` +git push \ --force +``` +强制推送 +``` +git push \ --all +``` + +本地所有的分支推送到远程仓库当中 + +``` +git push \ --tags +``` + +将本地所有标签推送到远程仓库中 diff --git a/Java/Java基础/Java IO.md b/Java/Java基础/Java IO.md new file mode 100644 index 00000000..d95eee4f --- /dev/null +++ b/Java/Java基础/Java IO.md @@ -0,0 +1,622 @@ +# Java IO + +* [Java IO](#java-io) + * [一、概览](#一概览) + * [二、磁盘操作](#二磁盘操作) + * [三、字节操作](#三字节操作) + * [实现文件复制](#实现文件复制) + * [装饰者模式](#装饰者模式) + * [四、字符操作](#四字符操作) + * [编码与解码](#编码与解码) + * [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 + +## 二、磁盘操作 + +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); + } +} +``` + +从 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); + + 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); + } + + in.close(); + out.close(); +} +``` + +### 装饰者模式 + +Java I/O 使用了装饰者模式来实现。以 InputStream 为例, + +- InputStream 是抽象组件; +- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; +- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 + +

+ +实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 + +```java +FileInputStream fileInputStream = new FileInputStream(filePath); +BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); +``` + +DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。 + +## 四、字符操作 + +### 编码与解码 + +编码就是把字符转换为字节,而解码是把字节重新组合成字符。 + +如果编码和解码过程使用不同的编码方式那么就出现了乱码。 + +- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节; +- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节; +- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。 + +UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。 + +Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。 + +### String 的编码方式 + +String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。 + +```java +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。 + +```java +byte[] bytes = str1.getBytes(); +``` + +### Reader 与 Writer + +不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。 + +- InputStreamReader 实现从字节流解码成字符流; +- OutputStreamWriter 实现字符流编码成为字节流。 + +### 实现逐行输出文本文件的内容 + +```java +public static void readFileContent(String filePath) throws IOException { + + FileReader fileReader = new FileReader(filePath); + BufferedReader bufferedReader = new BufferedReader(fileReader); + + String line; + while ((line = bufferedReader.readLine()) != null) { + System.out.println(line); + } + + // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象 + // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法 + // 因此只要一个 close() 调用即可 + bufferedReader.close(); +} +``` + +## 五、对象操作 + +### 序列化 + +序列化就是将一个对象转换成字节序列,方便存储和传输。 + +- 序列化:ObjectOutputStream.writeObject() +- 反序列化:ObjectInputStream.readObject() + +不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。 + +### Serializable + +序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。 + +```java +public static void main(String[] args) throws IOException, ClassNotFoundException { + + A a1 = new A(123, "abc"); + String objectFile = "file/a1"; + + 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); +} + +private static class A implements Serializable { + + private int x; + private String y; + + A(int x, String y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return "x = " + x + " " + "y = " + y; + } +} +``` + +### transient + +transient 关键字可以使一些属性不会被序列化。 + +ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 + +```java +private transient Object[] elementData; +``` + +## 六、网络操作 + +Java 中的网络支持: + +- InetAddress:用于表示网络上的硬件资源,即 IP 地址; +- URL:统一资源定位符; +- Sockets:使用 TCP 协议实现网络通信; +- Datagram:使用 UDP 协议实现网络通信。 + +### InetAddress + +没有公有的构造函数,只能通过静态方法来创建实例。 + +```java +InetAddress.getByName(String host); +InetAddress.getByAddress(byte[] address); +``` + +### URL + +可以直接从 URL 中读取字节流数据。 + +```java +public static void main(String[] args) throws IOException { + + URL url = new URL("http://www.baidu.com"); + + /* 字节流 */ + InputStream is = url.openStream(); + + /* 字符流 */ + InputStreamReader isr = new InputStreamReader(is, "utf-8"); + + /* 提供缓存功能 */ + BufferedReader br = new BufferedReader(isr); + + String line; + while ((line = br.readLine()) != null) { + System.out.println(line); + } + + br.close(); +} +``` + +### Sockets + +- ServerSocket:服务器端类 +- Socket:客户端类 +- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。 + +

+ +### Datagram + +- DatagramSocket:通信类 +- DatagramPacket:数据包类 + +## 七、NIO + +新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 + +### 流与块 + +I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 + +面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。 + +面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 + +I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 + +### 通道与缓冲区 + +#### 1. 通道 + +通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 + +通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 + +通道包括以下类型: + +- FileChannel:从文件中读写数据; +- DatagramChannel:通过 UDP 读写网络中数据; +- SocketChannel:通过 TCP 读写网络中数据; +- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 + +#### 2. 缓冲区 + +发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 + +缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 + +缓冲区包括以下类型: + +- ByteBuffer +- CharBuffer +- ShortBuffer +- IntBuffer +- LongBuffer +- FloatBuffer +- DoubleBuffer + +### 缓冲区状态变量 + +- capacity:最大容量; +- position:当前已经读写的字节数; +- limit:还可以读写的字节数。 + +状态变量的改变过程举例: + +① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。 + +

+ +② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。 + +

+ +③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 + +

+ +④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。 + +

+ +⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。 + +

+ +### 文件 NIO 实例 + +以下展示了使用 NIO 快速复制文件的实例: + +```java +public static void fastCopy(String src, String dist) throws IOException { + + /* 获得源文件的输入字节流 */ + FileInputStream fin = new FileInputStream(src); + + /* 获取输入字节流的文件通道 */ + FileChannel fcin = fin.getChannel(); + + /* 获取目标文件的输出字节流 */ + FileOutputStream fout = new FileOutputStream(dist); + + /* 获取输出字节流的文件通道 */ + FileChannel fcout = fout.getChannel(); + + /* 为缓冲区分配 1024 个字节 */ + ByteBuffer buffer = ByteBuffer.allocateDirect(1024); + + while (true) { + + /* 从输入通道中读取数据到缓冲区中 */ + int r = fcin.read(buffer); + + /* read() 返回 -1 表示 EOF */ + if (r == -1) { + break; + } + + /* 切换读写 */ + buffer.flip(); + + /* 把缓冲区的内容写入输出文件中 */ + fcout.write(buffer); + + /* 清空缓冲区 */ + buffer.clear(); + } +} +``` + +### 选择器 + +NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。 + +NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。 + +通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。 + +因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。 + +应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。 + +

+ +#### 1. 创建选择器 + +```java +Selector selector = Selector.open(); +``` + +#### 2. 将通道注册到选择器上 + +```java +ServerSocketChannel ssChannel = ServerSocketChannel.open(); +ssChannel.configureBlocking(false); +ssChannel.register(selector, SelectionKey.OP_ACCEPT); +``` + +通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。 + +在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类: + +- SelectionKey.OP_CONNECT +- SelectionKey.OP_ACCEPT +- SelectionKey.OP_READ +- SelectionKey.OP_WRITE + +它们在 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; +``` + +可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如: + +```java +int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; +``` + +#### 3. 监听事件 + +```java +int num = selector.select(); +``` + +使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。 + +#### 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(); +} +``` + +#### 5. 事件循环 + +因为一次 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(); + } +} +``` + +### 套接字 NIO 实例 + +```java +public class NIOServer { + + public static void main(String[] args) throws IOException { + + Selector selector = Selector.open(); + + 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); + + while (true) { + + selector.select(); + Set keys = selector.selectedKeys(); + Iterator keyIterator = keys.iterator(); + + while (keyIterator.hasNext()) { + + SelectionKey key = keyIterator.next(); + + if (key.isAcceptable()) { + + ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); + + // 服务器会为每个新连接创建一个 SocketChannel + SocketChannel sChannel = ssChannel1.accept(); + sChannel.configureBlocking(false); + + // 这个新连接主要用于从客户端读取数据 + sChannel.register(selector, SelectionKey.OP_READ); + + } else if (key.isReadable()) { + + SocketChannel sChannel = (SocketChannel) key.channel(); + System.out.println(readDataFromSocketChannel(sChannel)); + sChannel.close(); + } + + keyIterator.remove(); + } + } + } + + private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { + + ByteBuffer buffer = ByteBuffer.allocate(1024); + StringBuilder data = new StringBuilder(); + + 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(); + } +} +``` + +```java +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(); + } +} +``` + +### 内存映射文件 + +内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 + +向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 + +下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 + +```java +MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); +``` + +### 对比 + +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) diff --git a/Java/Java基础/Java 基础.md b/Java/Java基础/Java 基础.md new file mode 100644 index 00000000..18ced0b3 --- /dev/null +++ b/Java/Java基础/Java 基础.md @@ -0,0 +1,1462 @@ +# Java 基础 + +* [Java 基础](#java-基础) + * [一、数据类型](#一数据类型) + * [基本类型](#基本类型) + * [包装类型](#包装类型) + * [缓存池](#缓存池) + * [二、String](#二string) + * [概览](#概览) + * [不可变的好处](#不可变的好处) + * [String, StringBuffer and StringBuilder ](#string-stringbuffer-and-stringbuilder ) + * [String Pool](#string-pool) + * [new String("abc")](#new-stringabc) + * [三、运算](#三运算) + * [参数传递](#参数传递) + * [float 与 double](#float-与-double) + * [隐式类型转换](#隐式类型转换) + * [switch](#switch) + * [四、关键字](#四关键字) + * [final](#final) + * [static](#static) + * [五、Object 通用方法](#五object-通用方法) + * [概览](#概览) + * [equals()](#equals) + * [hashCode()](#hashcode) + * [toString()](#tostring) + * [clone()](#clone) + * [六、继承](#六继承) + * [访问权限](#访问权限) + * [抽象类与接口](#抽象类与接口) + * [super](#super) + * [重写与重载](#重写与重载) + * [七、反射](#七反射) + * [八、异常](#八异常) + * [九、泛型](#九泛型) + * [十、注解](#十注解) + * [十一、特性](#十一特性) + * [Java 各版本的新特性](#java-各版本的新特性) + * [Java 与 C++ 的区别](#java-与-c-的区别) + * [JRE or JDK](#jre-or-jdk) + * [参考资料](#参考资料) + + + +## 一、数据类型 + +### 基本类型 + +- 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) + +### 包装类型 + +基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。 + +```java +Integer x = 2; // 装箱 调用了 Integer.valueOf(2) +int y = x; // 拆箱 调用了 X.intValue() +``` + +- [Autoboxing and Unboxing](https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html) + +### 缓存池 + +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 +``` + +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); +} +``` + +在 Java 8 中,Integer 缓存池的大小默认为 -128\~127。 + +```java +static final int low = -128; +static final int high; +static final Integer cache[]; + +static { + // high value may be configured by property + int h = 127; + String integerCacheHighPropValue = + sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); + if (integerCacheHighPropValue != null) { + try { + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + // Maximum array size is Integer.MAX_VALUE + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + } catch( NumberFormatException nfe) { + // If the property cannot be parsed into an int, ignore it. + } + } + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert IntegerCache.high >= 127; +} +``` + +编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。 + +```java +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 + +在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。 + +在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。 + +[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 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承) + +在 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[]; +} +``` + +在 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; + + /** The identifier of the encoding used to encode the bytes in {@code value}. */ + private final byte coder; +} +``` + +value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。 + +### 不可变的好处 + +**1. 可以缓存 hash 值** + +因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 + +**2. String Pool 的需要** + +如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 + +

+ +**3. 安全性** + +String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。 + +**4. 线程安全** + +String 不可变性天生具备线程安全,可以在多个线程中安全地使用。 + +[Program Creek : Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/) + +### String, StringBuffer and StringBuilder + +**1. 可变性** + +- String 不可变 +- StringBuffer 和 StringBuilder 可变 + +**2. 线程安全** + +- String 不可变,因此是线程安全的 +- StringBuilder 不是线程安全的 +- StringBuffer 是线程安全的,内部使用 synchronized 进行同步 + +[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder) + +### String Pool + +字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。 + +当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。 + +下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 和 s2.intern() 方法取得同一个字符串引用。intern() 首先把 "aaa" 放到 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 = s2.intern(); +System.out.println(s3 == s4); // true +``` + +如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。 + +```java +String s5 = "bbb"; +String s6 = "bbb"; +System.out.println(s5 == s6); // true +``` + +在 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) + +### new String("abc") + +使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。 + +- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量; +- 而使用 new 的方式会在堆中创建一个字符串对象。 + +创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。 + +```java +public class NewStringTest { + public static void main(String[] args) { + String s = new String("abc"); + } +} +``` + +使用 javap -verbose 进行反编译,得到以下内容: + +```java +// ... +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 +// ... +``` + +在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。 + +以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。 + +```java +public String(String original) { + this.value = original.value; + this.hash = original.hash; +} +``` + +## 三、运算 + +### 参数传递 + +Java 的参数是以值传递的形式传入方法中,而不是引用传递。 + +以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。 + +```java +public class Dog { + + String name; + + Dog(String name) { + this.name = name; + } + + String getName() { + return this.name; + } + + void setName(String name) { + this.name = name; + } + + String getObjectAddress() { + return super.toString(); + } +} +``` + +在方法中改变对象的字段值会改变原对象该字段值,因为引用的是同一个对象。 + +```java +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"); + } +} +``` + +但是在方法中将指针引用了其它对象,那么此时方法里和方法外的两个指针指向了不同的对象,在一个指针改变其所指向对象的内容对另一个指针所指向的对象没有影响。 + +```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 + } + + 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 + } +} +``` + +[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 + +Java 不能隐式执行向下转型,因为这会使得精度降低。 + +1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。 + +```java +// float f = 1.1; +``` + +1.1f 字面量才是 float 类型。 + +```java +float f = 1.1f; +``` + +### 隐式类型转换 + +因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型。 + +```java +short s1 = 1; +// s1 = s1 + 1; +``` + +但是使用 += 或者 ++ 运算符会执行隐式类型转换。 + +```java +s1 += 1; +s1++; +``` + +上面的语句相当于将 s1 + 1 的计算结果进行了向下转型: + +```java +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) + +### switch + +从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。 + +```java +String s = "a"; +switch (s) { + case "a": + System.out.println("aaa"); + break; + case "b": + System.out.println("bbb"); + break; +} +``` + +switch 不支持 long、float、double,是因为 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; +// } +``` + +[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) + + +## 四、关键字 + +### final + +**1. 数据** + +声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。 + +- 对于基本类型,final 使数值不变; +- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 + +```java +final int x = 1; +// x = 2; // cannot assign value to final variable 'x' +final A y = new A(); +y.a = 1; +``` + +**2. 方法** + +声明方法不能被子类重写。 + +private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。 + +**3. 类** + +声明类不允许被继承。 + +### static + +**1. 静态变量** + +- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。 +- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 + +```java +public class A { + + 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; + } +} +``` + +**2. 静态方法** + +静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。 + +```java +public abstract class A { + public static void func1(){ + } + // public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static' +} +``` + +只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因此这两个关键字与具体对象关联。 + +```java +public class A { + + 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 + } +} +``` + +**3. 静态语句块** + +静态语句块在类初始化时运行一次。 + +```java +public class A { + static { + System.out.println("123"); + } + + public static void main(String[] args) { + A a1 = new A(); + A a2 = new A(); + } +} +``` + +```html +123 +``` + +**4. 静态内部类** + +非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。 + +```java +public class OuterClass { + + class InnerClass { + } + + 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(); + } +} +``` + +静态内部类不能访问外部类的非静态的变量和方法。 + +**5. 静态导包** + +在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 + +```java +import static com.xxx.ClassName.* +``` + +**6. 初始化顺序** + +静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。 + +```java +public static String staticField = "静态变量"; +``` + +```java +static { + System.out.println("静态语句块"); +} +``` + +```java +public String field = "实例变量"; +``` + +```java +{ + System.out.println("普通语句块"); +} +``` + +最后才是构造函数的初始化。 + +```java +public InitialOrderTest() { + System.out.println("构造函数"); +} +``` + +存在继承的情况下,初始化顺序为: + +- 父类(静态变量、静态语句块) +- 子类(静态变量、静态语句块) +- 父类(实例变量、普通语句块) +- 父类(构造函数) +- 子类(实例变量、普通语句块) +- 子类(构造函数) + +## 五、Object 通用方法 + +### 概览 + +```java + +public native int hashCode() + +public boolean equals(Object obj) + +protected native Object clone() throws CloneNotSupportedException + +public String toString() + +public final native Class getClass() + +protected void finalize() throws Throwable {} + +public final native void notify() + +public final native void notifyAll() + +public final native void wait(long timeout) throws InterruptedException + +public final void wait(long timeout, int nanos) throws InterruptedException + +public final void wait() throws InterruptedException +``` + +### equals() + +**1. 等价关系** + +两个对象具有等价关系,需要满足以下五个条件: + +Ⅰ 自反性 + +```java +x.equals(x); // true +``` + +Ⅱ 对称性 + +```java +x.equals(y) == y.equals(x); // true +``` + +Ⅲ 传递性 + +```java +if (x.equals(y) && y.equals(z)) + x.equals(z); // true; +``` + +Ⅳ 一致性 + +多次调用 equals() 方法结果不变 + +```java +x.equals(y) == x.equals(y); // true +``` + +Ⅴ 与 null 的比较 + +对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false + +```java +x.equals(null); // false; +``` + +**2. 等价与相等** + +- 对于基本类型,== 判断两个值是否相等,基本类型没有 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 +``` + +**3. 实现** + +- 检查是否为同一个对象的引用,如果是直接返回 true; +- 检查是否是同一个类型,如果不是,直接返回 false; +- 将 Object 对象进行转型; +- 判断每个关键域是否相等。 + +```java +public class EqualExample { + + 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; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + EqualExample that = (EqualExample) o; + + if (x != that.x) return false; + if (y != that.y) return false; + return z == that.z; + } +} +``` + +### hashCode() + +hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。 + +在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等。 + +HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。 + +下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象。但是 EqualExample 没有实现 hashCode() 方法,因此这两个对象的哈希值是不同的,最终导致集合添加了两个等价的对象。 + +```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<>(); +set.add(e1); +set.add(e2); +System.out.println(set.size()); // 2 +``` + +理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。 + +R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失。并且一个数与 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; +} +``` + +### toString() + +默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。 + +```java +public class ToStringExample { + + private int number; + + public ToStringExample(int number) { + this.number = number; + } +} +``` + +```java +ToStringExample example = new ToStringExample(123); +System.out.println(example.toString()); +``` + +```html +ToStringExample@4554617c +``` + +### clone() + +**1. cloneable** + +clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。 + +```java +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' +``` + +重写 clone() 得到以下实现: + +```java +public class CloneExample { + private int a; + private int b; + + @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(); +} +``` + +```html +java.lang.CloneNotSupportedException: CloneExample +``` + +以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。 + +应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 + +```java +public class CloneExample implements Cloneable { + private int a; + private int b; + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} +``` + +**2. 浅拷贝** + +拷贝对象和原始对象的引用类型引用同一个对象。 + +```java +public class ShallowCloneExample implements Cloneable { + + private int[] arr; + + 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 int get(int index) { + return arr[index]; + } + + @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(); +} +e1.set(2, 222); +System.out.println(e2.get(2)); // 222 +``` + +**3. 深拷贝** + +拷贝对象和原始对象的引用类型引用不同对象。 + +```java +public class DeepCloneExample implements Cloneable { + + private int[] arr; + + 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 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; + } +} +``` + +```java +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 +``` + +**4. clone() 的替代方案** + +使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。 + +```java +public class CloneConstructorExample { + + private int[] arr; + + 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 void set(int index, int value) { + arr[index] = value; + } + + 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 +``` + +## 六、继承 + +### 访问权限 + +Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。 + +可以对类或类中的成员(字段和方法)加上访问修饰符。 + +- 类可见表示其它类可以用这个类创建实例对象。 +- 成员可见表示其它类可以用这个类的实例对象访问到该成员; + +protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。 + +设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。 + +如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例去代替,也就是确保满足里氏替换原则。 + +字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。 + +```java +public class AccessExample { + public String id; +} +``` + +可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。 + +```java +public class AccessExample { + + private int id; + + public String getId() { + return id + ""; + } + + public void setId(String id) { + this.id = Integer.valueOf(id); + } +} +``` + +但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。 + +```java +public class AccessWithInnerClassExample { + + private class InnerClass { + int x; + } + + private InnerClass innerClass; + + public AccessWithInnerClassExample() { + innerClass = new InnerClass(); + } + + public int getValue() { + return innerClass.x; // 直接访问 + } +} +``` + +### 抽象类与接口 + +**1. 抽象类** + +抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。 + +抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。 + +```java +public abstract class AbstractClassExample { + + protected int x; + private int y; + + public abstract void func1(); + + public void func2() { + System.out.println("func2"); + } +} +``` + +```java +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(); +ac2.func1(); +``` + +**2. 接口** + +接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。 + +从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类,让它们都实现新增的方法。 + +接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。从 Java 9 开始,允许将方法定义为 private,这样就能定义某些复用的代码又不会把方法暴露出去。 + +接口的字段默认都是 static 和 final 的。 + +```java +public interface InterfaceExample { + + void func1(); + + 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 +} +``` + +```java +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(); +ie2.func1(); +System.out.println(InterfaceExample.x); +``` + +**3. 比较** + +- 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。 +- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 +- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。 +- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。 + +**4. 使用选择** + +使用接口: + +- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Comparable 接口中的 compareTo() 方法; +- 需要使用多重继承。 + +使用抽象类: + +- 需要在几个相关的类中共享代码。 +- 需要能控制继承来的成员的访问权限,而不是都为 public。 +- 需要继承非静态和非常量字段。 + +在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 + +- [Abstract Methods and Classes](https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html) +- [深入理解 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) +- [Java 9 Private Methods in Interfaces](https://www.journaldev.com/12850/java-9-private-methods-interfaces) + + +### super + +- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用 super() 函数。 +- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。 + +```java +public class SuperExample { + + protected int x; + protected int y; + + public SuperExample(int x, int y) { + this.x = x; + this.y = y; + } + + public void func() { + System.out.println("SuperExample.func()"); + } +} +``` + +```java +public class SuperExtendExample extends SuperExample { + + private int 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()"); + } +} +``` + +```java +SuperExample e = new SuperExtendExample(1, 2, 3); +e.func(); +``` + +```html +SuperExample.func() +SuperExtendExample.func() +``` + +[Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) + +### 重写与重载 + +**1. 重写(Override)** + +存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。 + +为了满足里式替换原则,重写有以下三个限制: + +- 子类方法的访问权限必须大于等于父类方法; +- 子类方法的返回类型必须是父类方法返回类型或为其子类型。 +- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。 + +使用 @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<>(); + } +} +``` + +在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有再到父类中查看,看是否从父类继承来。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为: + +- this.func(this) +- super.func(this) +- this.func(super) +- super.func(super) + + +```java +/* + A + | + B + | + C + | + D + */ + + +class A { + + public void show(A obj) { + System.out.println("A.show(A)"); + } + + public void show(C obj) { + System.out.println("A.show(C)"); + } +} + +class B extends A { + + @Override + public void show(A obj) { + System.out.println("B.show(A)"); + } +} + +class C extends B { +} + +class D extends C { +} +``` + +```java +public static void main(String[] args) { + + A a = new A(); + B b = new B(); + C c = new C(); + D d = new D(); + + // 在 A 中存在 show(A obj),直接调用 + a.show(a); // A.show(A) + // 在 A 中不存在 show(B obj),将 B 转型成其父类 A + a.show(b); // A.show(A) + // 在 B 中存在从 A 继承来的 show(C obj),直接调用 + b.show(c); // A.show(C) + // 在 B 中不存在 show(D obj),但是存在从 A 继承来的 show(C obj),将 D 转型成其父类 C + b.show(d); // A.show(C) + + // 引用的还是 B 对象,所以 ba 和 b 的调用结果一样 + A ba = new B(); + ba.show(c); // A.show(C) + ba.show(d); // A.show(C) +} +``` + +**2. 重载(Overload)** + +存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。 + +应该注意的是,返回值不同,其它都相同不算是重载。 + +```java +class OverloadingExample { + public void show(int x) { + System.out.println(x); + } + + public void show(int x, String y) { + System.out.println(x + " " + y); + } +} +``` + +```java +public static void main(String[] args) { + OverloadingExample example = new OverloadingExample(); + example.show(1); + example.show(1, "2"); +} +``` + +## 七、反射 + +每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。 + +类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。 + +反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。 + +Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类: + +- **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段; +- **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法; +- **Constructor** :可以用 Constructor 的 newInstance() 创建新的对象。 + +**反射的优点:** + +- **可扩展性** :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。 +- **类浏览器和可视化开发环境** :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。 +- **调试器和测试工具** : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。 + +**反射的缺点:** + +尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。 + +- **性能开销** :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 + +- **安全限制** :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 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/) + +## 八、异常 + +Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种: + +- **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复; +- **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。 + +

+ +- [Java 入门之异常处理](https://www.cnblogs.com/Blue-Keroro/p/8875898.html) +- [Java Exception Interview Questions and Answers](https://www.journaldev.com/2167/java-exception-interview-questions-and-answersl) + +## 九、泛型 + +```java +public class Box { + // T stands for "Type" + private T t; + public void set(T t) { this.t = t; } + public T get() { return t; } +} +``` + +- [Java 泛型详解](https://www.cnblogs.com/Blue-Keroro/p/8875898.html) +- [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) + +## 十、注解 + +Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。 + +[注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html) + +## 十一、特性 + +### Java 各版本的新特性 + +**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 + +**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 + +- [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 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。 +- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。 +- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。 +- Java 支持自动垃圾回收,而 C++ 需要手动回收。 +- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。 +- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。 +- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。 + +[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:Java Runtime Environment,Java 运行环境的简称,为 Java 的运行提供了所需的环境。它是一个 JVM 程序,主要包括了 JVM 的标准实现和一些 Java 基本类库。 +- JDK:Java Development Kit,Java 开发工具包,提供了 Java 的开发及运行环境。JDK 是 Java 开发的核心,集成了 JRE 以及一些其它的工具,比如编译 Java 源码的编译器 javac 等。 + +## 参考资料 + +- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002. +- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017. diff --git a/Java/Java基础/Java 容器.md b/Java/Java基础/Java 容器.md new file mode 100644 index 00000000..b13bf7d5 --- /dev/null +++ b/Java/Java基础/Java 容器.md @@ -0,0 +1,1133 @@ +# Java 容器 + +* [Java 容器](#java-容器) + * [一、概览](#一概览) + * [Collection](#collection) + * [Map](#map) + * [二、容器中的设计模式](#二容器中的设计模式) + * [迭代器模式](#迭代器模式) + * [适配器模式](#适配器模式) + * [三、源码分析](#三源码分析) + * [ArrayList](#arraylist) + * [Vector](#vector) + * [CopyOnWriteArrayList](#copyonwritearraylist) + * [LinkedList](#linkedlist) + * [HashMap](#hashmap) + * [ConcurrentHashMap](#concurrenthashmap) + * [LinkedHashMap](#linkedhashmap) + * [WeakHashMap](#weakhashmap) + * [参考资料](#参考资料) + + + +## 一、概览 + +容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 + +### Collection + +

+ +#### 1. Set + +- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。 + +- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 + +- LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。 + +#### 2. List + +- ArrayList:基于动态数组实现,支持随机访问。 + +- Vector:和 ArrayList 类似,但它是线程安全的。 + +- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 + +#### 3. Queue + +- LinkedList:可以用它来实现双向队列。 + +- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 + +### Map + +

+ +- TreeMap:基于红黑树实现。 + +- HashMap:基于哈希表实现。 + +- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 + +- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 + + +## 二、容器中的设计模式 + +### 迭代器模式 + +

+ +Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 + +从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。 + +```java +List list = new ArrayList<>(); +list.add("a"); +list.add("b"); +for (String item : list) { + System.out.println(item); +} +``` + +### 适配器模式 + +java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 + +```java +@SafeVarargs +public static List asList(T... a) +``` + +应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 + +```java +Integer[] arr = {1, 2, 3}; +List list = Arrays.asList(arr); +``` + +也可以使用以下方式调用 asList(): + +```java +List list = Arrays.asList(1, 2, 3); +``` + +## 三、源码分析 + +如果没有特别说明,以下源码分析基于 JDK 1.8。 + +在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码。 + +### ArrayList + + +#### 1. 概览 + +因为 ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。 + +```java +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable +``` + +数组的默认大小为 10。 + +```java +private static final int DEFAULT_CAPACITY = 10; +``` + +

+ +#### 2. 扩容 + +添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,即 oldCapacity+oldCapacity/2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍左右。(oldCapacity 为偶数就是 1.5 倍,为奇数就是 1.5 倍-0.5) + +扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 + +```java +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} + +private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + ensureExplicitCapacity(minCapacity); +} + +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} + +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +#### 3. 删除元素 + +需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。 + +```java +public E remove(int index) { + rangeCheck(index); + modCount++; + E oldValue = elementData(index); + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, numMoved); + elementData[--size] = null; // clear to let GC do its work + return oldValue; +} +``` + +#### 4. 序列化 + +ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。 + +保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。 + +```java +transient Object[] elementData; // non-private to simplify nested class access +``` + +ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 + +```java +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + elementData = EMPTY_ELEMENTDATA; + + // Read in size, and any hidden stuff + s.defaultReadObject(); + + // Read in capacity + s.readInt(); // ignored + + if (size > 0) { + // be like clone(), allocate array based upon size not capacity + ensureCapacityInternal(size); + + Object[] a = elementData; + // Read in all elements in the proper order. + for (int i=0; i