写点什么

小师妹学 IO 系列文章集合 - 附 PDF 下载

发布于: 2021 年 07 月 27 日

java 中最最让人激动的部分就是 IO 和 NIO 了。IO 的全称是 input output,是 java 程序跟外部世界交流的桥梁,IO 指的是 java.io 包中的所有类,他们是从 java1.0 开始就存在的。NIO 叫做 new IO,是在 java1.4 中引入的新一代 IO。


IO 的本质是什么呢?它和 NIO 有什么区别呢?我们该怎么学习 IO 和 NIO 呢?


本系列将会借助小师妹的视角,详细讲述学习 java IO 的过程,希望大家能够喜欢。


小师妹何许人也?姓名不详,但是勤奋爱学,潜力无限,一起来看看吧。


本文的例子https://github.com/ddean2009/learn-java-io-nio


文章太长,大家可以直接下载本文 PDF:下载链接 java-io-all-in-one.pdf


第一章 IO 的本质 IO 的本质 IO 的作用就是从外部系统读取数据到 java 程序中,或者把 java 程序中输出的数据写回到外部系统。这里的外部系统可能是磁盘,网络流等等。


因为对所有的外部数据的处理都是由操作系统内核来实现的,对于 java 应用程序来说,只是调用操作系统中相应的接口方法,从而和外部数据进行交互。


所有 IO 的本质就是对 Buffer 的处理,我们把数据放入 Buffer 供系统写入外部数据,或者从系统 Buffer 中读取从外部系统中读取的数据。如下图所示:


用户空间也就是我们自己的 java 程序有一个 Buffer,系统空间也有一个 buffer。所以会出现系统空间缓存数据的情况,这种情况下系统空间将会直接返回 Buffer 中的数据,提升读取速度。


DMA 和虚拟地址空间在继续讲解之前,我们先讲解两个操作系统中的基本概念,方便后面我们对 IO 的理解。


现代操作系统都有一个叫做 DMA(Direct memory access)的组件。这个组件是做什么的呢?


一般来说对内存的读写都是要交给 CPU 来完成的,在没有 DMA 的情况下,如果程序进行 IO 操作,那么所有的 CPU 时间都会被占用,CPU 没法去响应其他的任务,只能等待 IO 执行完成。这在现代应用程序中是无法想象的。


如果使用 DMA,则 CPU 可以把 IO 操作转交给其他的操作系统组件,比如数据管理器来操作,只有当数据管理器操作完毕之后,才会通知 CPU 该 IO 操作完成。现代操作系统基本上都实现了 DMA。


虚拟地址空间也叫做(Virtual address space),为了不同程序的互相隔离和保证程序中地址的确定性,现代计算机系统引入了虚拟地址空间的概念。简单点讲可以看做是跟实际物理地址的映射,通过使用分段或者分页的技术,将实际的物理地址映射到虚拟地址空间。


对于上面的 IO 的基本流程图中,我们可以将系统空间的 buffer 和用户空间的 buffer 同时映射到虚拟地址空间的同一个地方。这样就省略了从系统空间拷贝到用户空间的步骤。速度会更快。


同时为了解决虚拟空间比物理内存空间大的问题,现代计算机技术一般都是用了分页技术。


分页技术就是将虚拟空间分为很多个 page,只有在需要用到的时候才为该 page 分配到物理内存的映射,这样物理内存实际上可以看做虚拟空间地址的缓存。


虚拟空间地址分页对 IO 的影响就在于,IO 的操作也是基于 page 来的。


比较常用的 page 大小有:1,024, 2,048, 和 4,096 bytes。


IO 的分类 IO 可以分为 File/Block IO 和 Stream I/O 两类。


对于 File/Block IO 来说,数据是存储在 disk 中,而 disk 是由 filesystem 来进行管理的。我们可以通过 filesystem 来定义 file 的名字,路径,文件属性等内容。


filesystem 通过把数据划分成为一个个的 data blocks 来进行管理。有些 blocks 存储着文件的元数据,有些 block 存储着真正的数据。


最后 filesystem 在处理数据的过程中,也进行了分页。filesystem 的分页大小可以跟内存分页的大小一致,或者是它的倍数,比如 2,048 或者 8,192 bytes 等。


并不是所有的数据都是以 block 的形式存在的,我们还有一类 IO 叫做 stream IO。


stream IO 就像是管道流,里面的数据是序列被消费的。


IO 和 NIO 的区别 java1.0 中的 IO 是流式 IO,它只能一个字节一个字节的处理数据,所以 IO 也叫做 Stream IO。


而 NIO 是为了提升 IO 的效率而生的,它是以 Block 的方式来读取数据的。


Stream IO 中,input 输入一个字节,output 就输出一个字节,因为是 Stream,所以可以加上过滤器或者过滤器链,可以想想一下 web 框架中的 filter chain。在 Stream IO 中,数据只能处理一次,你不能在 Stream 中回退数据。


在 Block IO 中,数据是以 block 的形式来被处理的,因此其处理速度要比 Stream IO 快,同时可以回退处理数据。但是你需要自己处理 buffer,所以复杂程度要比 Stream IO 高。


一般来说 Stream IO 是阻塞型 IO,当线程进行读或者写操作的时候,线程会被阻塞。


而 NIO 一般来说是非阻塞的,也就是说在进行读或者写的过程中可以去做其他的操作,而读或者写操作执行完毕之后会通知 NIO 操作的完成。


在 IO 中,主要分为 DataOutPut 和 DataInput,分别对应 IO 的 out 和 in。


DataOutPut 有三大类,分别是 Writer,OutputStream 和 ObjectOutput。


看下他们中的继承关系:


DataInput 也有三大类,分别是 ObjectInput,InputStream 和 Reader。


看看他们的继承关系:


ObjectOutput 和 ObjectInput 类比较少,这里就不列出来了。


统计一下大概 20 个类左右,搞清楚这 20 个类的用处,恭喜你 java IO 你就懂了!


对于 NIO 来说比较复杂一点,首先,为了处理 block 的信息,需要将数据读取到 buffer 中,所以在 NIO 中 Buffer 是一个非常中要的概念,我们看下 NIO 中的 Buffer:


从上图我们可以看到 NIO 中为我们准备了各种各样的 buffer 类型使用。


另外一个非常重要的概念是 channel,channel 是 NIO 获取数据的通道:


NIO 需要掌握的类的个数比 IO 要稍稍多一点,毕竟 NIO 要复杂一点。


就这么几十个类,我们就掌握了 IO 和 NIO,想想都觉得兴奋。


总结后面的文章中,我们会介绍小师妹给你们认识,刚好她也在学 java IO,后面的学习就跟她一起进行吧,敬请期待。


第二章 try with 和它的底层原理简介小师妹是个 java 初学者,最近正在学习使用 java IO,作为大师兄的我自然要给她最给力的支持了。一起来看看她都遇到了什么问题和问题是怎么被解决的吧。


IO 关闭的问题这一天,小师妹一脸郁闷的问我:F 师兄,我学 Java IO 也有好多天了,最近写了一个例子,读取一个文件没有问题,但是读取很多个文件就会告诉我:”Can’t open so many files“,能帮我看看是什么问题吗?


更多内容请访问 www.flydean.com


小师妹的要求当然不能拒绝,我立马响应:可能打开文件太多了吧,教你两个命令,查看最大文件打开限制。


一个命令是 ulimit -a


第二个命令是


ulimit -n256 看起来是你的最大文件限制太小了,只有 256 个,调大一点就可以了。


小师妹却说:不对呀 F 师兄,我读文件都是一个一个读的,没有同时开这么多文件哟。


好吧,看下你写的代码吧:


BufferedReader bufferedReader = null;try {String line;bufferedReader = new BufferedReader(new FileReader("trywith/src/main/resources/www.flydean.com"));while ((line = bufferedReader.readLine()) != null) {log.info(line);}} catch (IOException e) {log.error(e.getMessage(), e);}看完代码,问题找到了,小师妹,你的 IO 没有关闭,应该在使用之后,在 finally 里面把你的 reader 关闭。


下面这段代码就行了:


BufferedReader bufferedReader = null;try {String line;bufferedReader = new BufferedReader(new FileReader("trywith/src/main/resources/www.flydean.com"));while ((line = bufferedReader.readLine()) != null) {log.info(line);}} catch (IOException e) {log.error(e.getMessage(), e);} finally {try {if (bufferedReader != null){bufferedReader.close();}} catch (IOException ex) {log.error(ex.getMessage(), ex);}}小师妹道了一声谢,默默的去改代码了。


使用 try with resource 过了半个小时 ,小师妹又来找我了,F 师兄,现在每段代码都要手动添加 finally,实在是太麻烦了,很多时候我又怕忘记关闭 IO 了,导致程序出现无法预料的异常。你也知道我这人从来就怕麻烦,有没有什么简单的办法,可以解决这个问题呢?


那么小师妹你用的 JDK 版本是多少?


小师妹不好意思的说:虽然最新的 JDK 已经到 14 了,我还是用的 JDK8.


JDK8 就够了,其实从 JDK7 开始,Java 引入了 try with resource 的新功能,你把使用过后要关闭的 resource 放到 try 里面,JVM 会帮你自动 close 的,是不是很方便,来看下面这段代码:


try (BufferedReader br = new BufferedReader(new FileReader("trywith/src/main/resources/www.flydean.com"))){String sCurrentLine;while ((sCurrentLine = br.readLine()) != null){log.info(sCurrentLine);}} catch (IOException e) {log.error(e.getMessage(), e);}try with resource 的原理太棒了,小师妹非常开心,然后又开始问我了:F 师兄,什么是 resource 呀?为什么放到 try 里面就可以不用自己 close 了?


resource 就是资源,可以打开个关闭,我们可以把实现了 java.lang.AutoCloseable 接口的类都叫做 resource。


先看下 AutoCloseable 的定义:


public interface AutoCloseable {void close() throws Exception;}AutoCloseable 定义了一个 close()方法,当我们在 try with resource 中打开了 AutoCloseable 的资源,那么当 try block 执行结束的时候,JVM 会自动调用这个 close()方法来关闭资源。


我们看下上面的 BufferedReader 中 close 方法是怎么实现的:


public void close() throws IOException {synchronized (lock) {if (in == null)return;in.close();in = null;cb = null;}}自定义 resource 小师妹恍然大悟:F 师兄,那么我们是不是可以实现 AutoCloseable 来创建自己的 resource 呢?


当然可以了,我们举个例子,比如给你解答完这个问题,我就要去吃饭了,我们定义这样一个 resource 类:


public class CustResource implements AutoCloseable {


public void helpSister(){    log.info("帮助小师妹解决问题!");}
@Overridepublic void close() throws Exception { log.info("解决完问题,赶紧去吃饭!");}
public static void main(String[] args) throws Exception { try( CustResource custResource= new CustResource()){ custResource.helpSister(); }}
复制代码


}运行输出结果:


[main] INFO com.flydean.CustResource - 帮助小师妹解决问题![main] INFO com.flydean.CustResource - 解决完问题,赶紧去吃饭!总结最后,小师妹的问题解决了,我也可以按时吃饭了。


第三章 File 文件系统简介小师妹又遇到难题了,这次的问题是有关文件的创建,文件权限和文件系统相关的问题,还好这些问题的答案都在我的脑子里面,一起来看看吧。


文件权限和文件系统早上刚到公司,小师妹就凑过来神神秘秘的问我:F 师兄,我在服务器上面放了一些重要的文件,是非常非常重要的那种,有没有什么办法给它加个保护,还兼顾一点隐私?


更多内容请访问 www.flydean.com


什么文件这么重要呀?不会是你的照片吧,放心没人会感兴趣的。


小师妹说:当然不是,我要把我的学习心得放上去,但是 F 师兄你知道的,我刚刚开始学习,很多想法都不太成熟,想先保个密,后面再公开。


看到小师妹这么有上进心,我老泪纵横,心里很是安慰。那就开始吧。


你知道,这个世界上操作系统分为两类,windows 和 linux(unix)系统。两个系统是有很大区别的,但两个系统都有一个文件的概念,当然 linux 中文件的范围更加广泛,几乎所有的资源都可以看做是文件。


有文件就有对应的文件系统,这些文件系统是由系统内核支持的,并不需要我们在 java 程序中重复造轮子,直接调用系统的内核接口就可以了。


小师妹:F 师兄,这个我懂,我们不重复造轮子,我们只是轮子的搬运工。那么 java 是怎么调用系统内核来创建文件的呢?


创建文件最常用的方法就是调用 File 类中的 createNewFile 方法,我们看下这个方法的实现:


public boolean createNewFile() throws IOException {SecurityManager security = System.getSecurityManager();if (security != null) security.checkWrite(path);if (isInvalid()) {throw new IOException("Invalid file path");}return fs.createFileExclusively(path);}方法内部先进行了安全性检测,如果通过了安全性检测就会调用 FileSystem 的 createFileExclusively 方法来创建文件。


在我的 mac 环境中,FileSystem 的实现类是 UnixFileSystem:


public native boolean createFileExclusively(String path)throws IOException;看到了吗?UnixFileSystem 中的 createFileExclusively 是一个 native 方法,它会去调用底层的系统接口。


小师妹:哇,文件创建好了,我们就可以给文件赋权限了,但是 windows 和 linux 的权限是一样的吗?


这个问题问得好,java 代码是跨平台的,我们的代码需要同时在 windows 和 linux 上的 JVM 执行,所以必须找到他们权限的共同点。


我们先看一下 windows 文件的权限:


可以看到一个 windows 文件的权限可以有修改,读取和执行三种,特殊权限我们先不用考虑,因为我们需要找到 windows 和 linux 的共同点。


再看下 linux 文件的权限:


ls -al www.flydean.com-rw-r--r-- 1 flydean staff 15 May 14 15:43 www.flydean.com 上面我使用了一个 ll 命令列出了 www.flydean.com 这个文件的详细信息。 其中第一列就是文件的权限了。


linux 的基本文件权限可以分为三部分,分别是 owner,group,others,每部分和 windows 一样都有读,写和执行的权限,分别用 rwx 来表示。


三部分的权限连起来就成了 rwxrwxrwx,对比上面我们的输出结果,我们可以看到 www.flydean.com 这个文件对 owner 自己是可读写的,对 Group 用户是只读的,对 other 用户也是只读的。


你要想把文件只对自己可读,那么可以执行下面的命令:


chmod 600 www.flydean.com 小师妹立马激动起来:F 师兄,这个我懂,6 用二进制表示就是 110,600 用二进制表示就是 110000000,刚刚好对应 rw——-。


对于小师妹的领悟能力,我感到非常满意。


文件的创建虽然我们已经不是孔乙己时代了,不需要知道茴字的四种写法,但是多一条知识多一条路,做些充足的准备还是非常有必要的。


小师妹,那你知道在 java 中有哪几种文件的创建方法呢?


小师妹小声道:F 师兄,我只知道一种 new File 的方法。


我满意的抚摸着我的胡子,显示一下自己高人的气场。


之前我们讲过了,IO 有三大类,一种是 Reader/Writer,一种是 InputStream/OutputStream,最后一种是 ObjectReader/ObjectWriter。


除了使用第一种 new File 之外,我们还可以使用 OutputStream 来实现,当然我们还要用到之前讲到 try with resource 特性,让代码更加简洁。


先看第一种方式:


public void createFileWithFile() throws IOException {File file = new File("file/src/main/resources/www.flydean.com");//Create the fileif (file.createNewFile()){log.info("恭喜,文件创建成功");}else{log.info("不好意思,文件创建失败");}//Write Contenttry(FileWriter writer = new FileWriter(file)){writer.write("www.flydean.com");}}再看第二种方式:


public void createFileWithStream() throws IOException{String data = "www.flydean.com";try(FileOutputStream out = new FileOutputStream("file/src/main/resources/www.flydean.com")){out.write(data.getBytes());}}第二种方式看起来比第一种方式更加简介。


小师妹:慢着,F 师兄,JDK7 中 NIO 就已经出现了,能不能使用 NIO 来创建文件呢?


这个问题当然难不到我:


public void createFileWithNIO() throws IOException{String data = "www.flydean.com";Files.write(Paths.get("file/src/main/resources/www.flydean.com"), data.getBytes());


    List<String> lines = Arrays.asList("程序那些事", "www.flydean.com");    Files.write(Paths.get("file/src/main/resources/www.flydean.com"),            lines,            StandardCharsets.UTF_8,            StandardOpenOption.CREATE,            StandardOpenOption.APPEND);}
复制代码


NIO 中提供了 Files 工具类来实现对文件的写操作,写的时候我们还可以带点参数,比如字符编码,是替换文件还是在 append 到文件后面等等。


代码中文件的权限小师妹又有问题了:F 师兄,讲了半天,还没有给我讲权限的事情啦。


别急,现在就讲权限:


public void fileWithPromission() throws IOException {File file = File.createTempFile("file/src/main/resources/www.flydean.com","");log.info("{}",file.exists());


    file.setExecutable(true);    file.setReadable(true,true);    file.setWritable(true);    log.info("{}",file.canExecute());    log.info("{}",file.canRead());    log.info("{}",file.canWrite());
Path path = Files.createTempFile("file/src/main/resources/www.flydean.com", ""); log.info("{}",Files.exists(path)); log.info("{}",Files.isReadable(path)); log.info("{}",Files.isWritable(path)); log.info("{}",Files.isExecutable(path));}
复制代码


上面我们讲过了,JVM 为了通用,只能取 windows 和 linux 都有的功能,那就是说权限只有读写和执行权限,因为 windows 里面也可以区分本用户或者其他用户,所以是否是本用户的权限也保留了。


上面的例子我们使用了传统的 File 和 NIO 中的 Files 来更新文件的权限。


总结好了,文件的权限就先讲到这里了。


第四章 文件读取那些事简介小师妹最新对 java IO 中的 reader 和 stream 产生了一点点困惑,不知道到底该用哪一个才对,怎么读取文件才是正确的姿势呢?今天 F 师兄现场为她解答。


字符和字节小师妹最近很迷糊:F 师兄,上次你讲到 IO 的读取分为两大类,分别是 Reader,InputStream,这两大类有什么区别吗?为什么我看到有些类即是 Reader 又是 Stream?比如:InputStreamReader?


小师妹,你知道哲学家的终极三问吗?你是谁?从哪里来?到哪里去?


F 师兄,你是不是迷糊了,我在问你 java,你扯什么哲学。


小师妹,其实吧,哲学是一切学问的基础,你知道科学原理的英文怎么翻译吗?the philosophy of science,科学的原理就是哲学。


你看计算机中代码的本质是什么?代码的本质就是 0 和 1 组成的一串长长的二进制数,这么多二进制数组合起来就成了计算机中的代码,也就是 JVM 可以识别可以运行的二进制代码。


更多内容请访问 www.flydean.com


小师妹一脸崇拜:F 师兄说的好像很有道理,但是这和 Reader,InputStream 有什么关系呢?


别急,冥冥中自有定数,先问你一个问题,java 中存储的最小单位是什么?


小师妹:容我想想,java 中最小的应该是 boolean,true 和 false 正好和二进制 1,0 对应。


对了一半,虽然 boolean 也是 java 中存储的最小单位,但是它需要占用一个字节 Byte 的空间。java 中最小的存储单位其实是字节 Byte。不信的话可以用之前我介绍的 JOL 工具来验证一下:


[main] INFO com.flydean.JolUsage - java.lang.Boolean object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 12 (object header) N/A12 1 boolean Boolean.value N/A13 3 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 3 bytes external = 3 bytes total 上面是装箱过后的 Boolean,可以看到虽然 Boolean 最后占用 16bytes,但是里面的 boolean 只有 1byte。


byte 翻译成中文就是字节,字节是 java 中存储的基本单位。


有了字节,我们就可以解释字符了,字符就是由字节组成的,根据编码方式的不同,字符可以有 1 个,2 个或者多个字节组成。我们人类可以肉眼识别的汉字呀,英文什么的都可以看做是字符。


而 Reader 就是按照一定编码格式读取的字符,而 InputStream 就是直接读取的更加底层的字节。


小师妹:我懂了,如果是文本文件我们就可以用 Reader,非文本文件我们就可以用 InputStream。


孺子可教,小师妹进步的很快。


按字符读取的方式小师妹,接下来 F 师兄给你讲下按字符读取文件的几种方式,第一种就是使用 FileReader 来读取 File,但是 FileReader 本身并没有提供任何读取数据的方法,想要真正的读取数据,我们还是要用到 BufferedReader 来连接 FileReader,BufferedReader 提供了读取的缓存,可以一次读取一行:


public void withFileReader() throws IOException {File file = new File("src/main/resources/www.flydean.com");


    try (FileReader fr = new FileReader(file); BufferedReader br = new BufferedReader(fr)) {        String line;        while ((line = br.readLine()) != null) {            if (line.contains("www.flydean.com")) {                log.info(line);            }        }    }}
复制代码


每次读取一行,可以把这些行连起来就组成了 stream,通过 Files.lines,我们获取到了一个 stream,在 stream 中我们就可以使用 lambda 表达式来读取文件了,这是谓第二种方式:


public void withStream() throws IOException {Path filePath = Paths.get("src/main/resources", "www.flydean.com");try (Stream<String> lines = Files.lines(filePath)){List<String> filteredLines = lines.filter(s -> s.contains("www.flydean.com")).collect(Collectors.toList());filteredLines.forEach(log::info);}}第三种其实并不常用,但是师兄也想教给你。这一种方式就是用工具类中的 Scanner。通过 Scanner 可以通过换行符来分割文件,用起来也不错:


public void withScanner() throws FileNotFoundException {FileInputStream fin = new FileInputStream(new File("src/main/resources/www.flydean.com"));Scanner scanner = new Scanner(fin,"UTF-8").useDelimiter("\n");String theString = scanner.hasNext() ? scanner.next() : "";log.info(theString);scanner.close();}按字节读取的方式小师妹听得很满足,连忙催促我:F 师兄,字符读取方式我都懂了,快将字节读取吧。


我点了点头,小师妹,哲学的本质还记得吗?字节就是 java 存储的本质。掌握到本质才能勘破一切虚伪。


还记得之前讲过的 Files 工具类吗?这个工具类提供了很多文件操作相关的方法,其中就有读取所有 bytes 的方法,小师妹要注意了,这里是一次性读取所有的字节!一定要慎用,只可用于文件较少的场景,切记切记。


public void readBytes() throws IOException {Path path = Paths.get("src/main/resources/www.flydean.com");byte[] data = Files.readAllBytes(path);log.info("{}",data);}如果是比较大的文件,那么可以使用 FileInputStream 来一次读取一定数量的 bytes:


public void readWithStream() throws IOException {File file = new File("src/main/resources/www.flydean.com");byte[] bFile = new byte[(int) file.length()];try(FileInputStream fileInputStream = new FileInputStream(file)){fileInputStream.read(bFile);for (int i = 0; i < bFile.length; i++) {log.info("{}",bFile[i]);}}}Stream 读取都是一个字节一个字节来读的,这样做会比较慢,我们使用 NIO 中的 FileChannel 和 ByteBuffer 来加快一些读取速度:


public void readWithBlock() throws IOException {try (RandomAccessFile aFile = new RandomAccessFile("src/main/resources/www.flydean.com", "r");FileChannel inChannel = aFile.getChannel();) {ByteBuffer buffer = ByteBuffer.allocate(1024);while (inChannel.read(buffer) > 0) {buffer.flip();for (int i = 0; i < buffer.limit(); i++) {log.info("{}", buffer.get());}buffer.clear();}}}小师妹:如果是非常非常大的文件的读取,有没有更快的方法呢?


当然有,记得上次我们讲过的虚拟地址空间的映射吧:


我们可以直接将用户的地址空间和系统的地址空间同时 map 到同一个虚拟地址内存中,这样就免除了拷贝带来的性能开销:


public void copyWithMap() throws IOException{try (RandomAccessFile aFile = new RandomAccessFile("src/main/resources/www.flydean.com", "r");FileChannel inChannel = aFile.getChannel()) {MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());buffer.load();for (int i = 0; i < buffer.limit(); i++){log.info("{}", buffer.get());}buffer.clear();}}寻找出错的行数小师妹:好赞!F 师兄你讲得真好,小师妹我还有一个问题:最近在做文件解析,有些文件格式不规范,解析到一半就解析失败了,但是也没有个错误提示到底错在哪一行,很难定位问题呀,有没有什么好的解决办法?


看看天色已经不早了,师兄就再教你一个方法,java 中有一个类叫做 LineNumberReader,使用它来读取文件可以打印出行号,是不是就满足了你的需求:


public void useLineNumberReader() throws IOException {try(LineNumberReader lineNumberReader = new LineNumberReader(new FileReader("src/main/resources/www.flydean.com"))){//输出初始行数 log.info("Line {}" , lineNumberReader.getLineNumber());//重置行数 lineNumberReader.setLineNumber(2);//获取现有行数 log.info("Line {} ", lineNumberReader.getLineNumber());//读取所有文件内容 String line = null;while ((line = lineNumberReader.readLine()) != null){log.info("Line {} is : {}" , lineNumberReader.getLineNumber() , line);}}}总结今天给小师妹讲解了字符流和字节流,还讲解了文件读取的基本方法,不虚此行。


第五章 文件写入那些事简介小师妹又对 F 师兄提了一大堆奇奇怪怪的需求,要格式化输出,要特定的编码输出,要自己定位输出,什么?还要阅后即焚?大家看 F 师兄怎么一一接招吧。


字符输出和字节输出小师妹:F 师兄,上次你的 IO 讲到了一半,文件读取是基本上讲完了,但是文件的写入还没有讲,什么时候给小师妹我再科普科普?


小师妹:F 师兄,你知道我这个人一直以来都是勤奋好学的典范,是老师们眼中的好学生,同学们心中的好榜样,父母身边乖巧的好孩子。在我永攀科学高峰的时候,居然发现还有一半的知识没有获取,真是让我扼腕叹息,F 师兄,快快把知识传给我吧。


小师妹你的请求,师兄我自当尽力办到,但是我怎么记得上次讲 IO 文件读取已经过了好几天了,怎么今天你才来找我。


小师妹红着脸:F 师兄,这不是使用的时候遇到了点问题,才想找你把知识再复习一遍。


那先把输出类的结构再过一遍:


上面就是输出的两大系统了:Writer 和 OutputStream。


Writer 主要针对于字符,而 Stream 主要针对 Bytes。


Writer 中最最常用的就是 FileWriter 和 BufferedWriter,我们看下一个最基本写入的例子:


public void useBufferedWriter() throws IOException {String content = "www.flydean.com";File file = new File("src/main/resources/www.flydean.com");


    FileWriter fw = new FileWriter(file);    try(BufferedWriter bw = new BufferedWriter(fw)){        bw.write(content);    }}
复制代码


BufferedWriter 是对 FileWriter 的封装,它提供了一定的 buffer 机制,可以提高写入的效率。


其实 BufferedWriter 提供了三种写入的方式:


public void write(int c)public void write(char cbuf[], int off, int len)public void write(String s, int off, int len)第一个方法传入一个 int,第二个方法传入字符数组和开始读取的位置和长度,第三个方法传入字符串和开始读取的位置和长度。是不是很简单,完全可以理解?


小师妹:不对呀,F 师兄,后面两个方法的参数,不管是 char 和 String 都是字符我可以理解,第一个方法传入 int 是什么鬼?


小师妹,之前跟你讲的道理是不是都忘记的差不多了,int 的底层存储是 bytes,char 和 String 的底层存储也是 bytes,我们把 int 和 char 做个强制转换就行了。我们看下是怎么转换的:


public void write(int c) throws IOException {synchronized (lock) {ensureOpen();if (nextChar >= nChars)flushBuffer();cb[nextChar++] = (char) c;}}还记得 int 需要占用多少个字节吗?4 个,char 需要占用 2 个字节。这样强制从 int 转换到 char 会有精度丢失的问题,只会保留低位的 2 个字节的数据,高位的两个字节的数据会被丢弃,这个需要在使用中注意。


看完 Writer,我们再来看看 Stream:


public void useFileOutputStream() throws IOException {String str = "www.flydean.com";try(FileOutputStream outputStream = new FileOutputStream("src/main/resources/www.flydean.com");BufferedOutputStream bufferedOutputStream= new BufferedOutputStream(outputStream)){byte[] strToBytes = str.getBytes();bufferedOutputStream.write(strToBytes);}}跟 Writer 一样,BufferedOutputStream 也是对 FileOutputStream 的封装,我们看下 BufferedOutputStream 中提供的 write 方法:


public synchronized void write(int b)public synchronized void write(byte b[], int off, int len)比较一下和 Writer 的区别,BufferedOutputStream 的方法是 synchronized 的,并且 BufferedOutputStream 是直接对 byte 进行操作的。


第一个 write 方法传入 int 参数也是需要进行截取的,不过这次是从 int 转换成 byte。


格式化输出小师妹:F 师兄,我们经常用的 System.out.println 可以直接向标准输出中输出格式化过后的字符串,文件的写入是不是也有类似的功能呢?


肯定有,PrintWriter 就是做格式化输出用的:


public void usePrintWriter() throws IOException {FileWriter fileWriter = new FileWriter("src/main/resources/www.flydean.com");try(PrintWriter printWriter = new PrintWriter(fileWriter)){printWriter.print("www.flydean.com");printWriter.printf("程序那些事 %s ", "非常棒");}}输出其他对象小师妹:F 师兄,我们看到可以输出 String,char 还有 Byte,那可不可以输出 Integer,Long 等基础类型呢?


可以的,使用 DataOutputStream 就可以做到:


public void useDataOutPutStream()throws IOException {String value = "www.flydean.com";try(FileOutputStream fos = new FileOutputStream("src/main/resources/www.flydean.com")){DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream(fos));outStream.writeUTF(value);}}DataOutputStream 提供了 writeLong,writeDouble,writeFloat 等等方法,还可以 writeUTF!


在特定的位置写入小师妹:F 师兄,有时候我们不需要每次都从头开始写入到文件,能不能自定义在什么位置写入呢?


使用 RandomAccessFile 就可以了:


public void useRandomAccess() throws IOException {try(RandomAccessFile writer = new RandomAccessFile("src/main/resources/www.flydean.com", "rw")){writer.seek(100);writer.writeInt(50);}}RandomAccessFile 可以通过 seek 来定位,然后通过 write 方法从指定的位置写入。


给文件加锁小师妹:F 师兄,最后还有一个问题,怎么保证我在进行文件写的时候别人不会覆盖我写的内容,不会产生冲突呢?


FileChannel 可以调用 tryLock 方法来获得一个 FileLock 锁,通过这个锁,我们可以控制文件的访问。


public void useFileLock()throws IOException {try(RandomAccessFile stream = new RandomAccessFile("src/main/resources/www.flydean.com", "rw");FileChannel channel = stream.getChannel()){FileLock lock = null;try {lock = channel.tryLock();} catch (final OverlappingFileLockException e) {stream.close();channel.close();}stream.writeChars("www.flydean.com");lock.release();}}总结今天给小师妹将了好多种文件的写的方法,够她学习一阵子了。


第六章 目录还是文件简介目录和文件傻傻分不清楚,目录和文件的本质到底是什么?在 java 中怎么操纵目录,怎么遍历目录。本文 F 师兄会为大家一一讲述。


linux 中的文件和目录小师妹:F 师兄,我最近有一个疑惑,java 代码中好像只有文件没有目录呀,是不是当初发明 java 的大神,一步小心走了神?


F 师兄:小师妹真勇气可嘉呀,敢于质疑权威是从小工到专家的最重要的一步。想想 F 师兄我,从小没人提点,老师讲什么我就信什么,专家说什么我就听什么:股市必上一万点,房子是给人住的不是给人炒的,原油宝当然是小白理财必备产品….然后,就没有然后了。


更多内容请访问 www.flydean.com


虽然 java 中没有目录的概念只有 File 文件,而 File 其实是可以表示目录的:


public boolean isDirectory()File 中有个 isDirectory 方法,可以判断该 File 是否是目录。


File 和目录傻傻分不清楚,小师妹,有没有联想到点什么?


小师妹:F 师兄,我记得你上次讲到 Linux 下面所有的资源都可以看做是文件,在 linux 下面文件和目录的本质是不是一样的?


对的,在 linux 下面文件是一等公民,所有的资源都是以文件的形式来区分的。


什么扇区,逻辑块,页之类的底层结构我们就不讲了。我们先考虑一下一个文件到底应该包含哪些内容。除了文件本身的数据之外,还有很多元数据的东西,比如文件权限,所有者,group,创建时间等信息。


在 linux 系统中,这两个部分是分开存储的。存放数据本身的叫做 block,存放元数据的叫做 inode。


inode 中存储了 block 的地址,可以通过 inode 找到文件实际数据存储的 block 地址,从而进行文件访问。考虑一下大文件可能占用很多个 block,所以一个 inode 中可以存储多个 block 的地址,而一个文件通常来说使用一个 inode 就够了。


为了显示层级关系和方便文件的管理,目录的数据文件中存放的是该目录下的文件和文件的 inode 地址,从而形成了一种一环套一环,圆环套圆环的链式关系。


上图列出了一个通过目录查找其下文件的环中环布局。


我想 java 中目录没有单独列出来一个类的原因可能是参考了 linux 底层的文件布局吧。


目录的基本操作因为在 java 中目录和文件是公用 File 这个类的,所以 File 的基本操作目录它全都会。


基本上,目录和文件相比要多注意下面三类方法:


public boolean isDirectory()public File[] listFiles()public boolean mkdir()为什么说是三类呢?因为还有几个和他们比较接近的方法,这里就不一一列举了。


isDirectory 判断该文件是不是目录。listFiles 列出该目录下面的所有文件。mkdir 创建一个文件目录。


小师妹:F 师兄,之前我们还以目录的遍历要耗费比较长的时间,经过你一讲解目录的数据结构,感觉 listFiles 并不是一个耗时操作呀,所有的数据都已经准备好了,直接读取出来就行。


对,看问题不要看表面,要看到隐藏在表面的本质内涵。你看师兄我平时不显山露水,其实是真正的中流砥柱,堪称公司优秀员工模范。


小师妹:F 师兄,那平时也没看上头表彰你啥的?哦,我懂了,一定是老板怕表彰了你引起别人的嫉妒,会让你的好好大师兄的形象崩塌吧,看来老板真的懂你呀。


目录的进阶操作好了小师妹,你懂了就行,下面 F 师兄给你讲一下目录的进阶操作,比如我们怎么拷贝一个目录呀?


小师妹,拷贝目录简单的 F 师兄,上次你就教我了:


cp -rf 一个命令的事情不就解决了吗?难道里面还隐藏了点秘密?


咳咳咳,秘密倒是没有,小师妹,我记得你上次说要对 java 从一而终的,今天师兄给你介绍一个在 java 中拷贝文件目录的方法。


其实 Files 工具类里已经为我们提供了一个拷贝文件的优秀方法:


public static Path copy(Path source, Path target, CopyOption... options)使用这个方法,我们就可以进行文件的拷贝了。


如果想要拷贝目录,就遍历目录中的文件,循环调用这个 copy 方法就够了。


小师妹:且慢,F 师兄,如果目录下面还有目录的,目录下还套目录的情况该怎么处理?


这就是圈套呀,看我用个递归的方法解决它:


public void useCopyFolder() throws IOException {File sourceFolder = new File("src/main/resources/flydean-source");File destinationFolder = new File("src/main/resources/flydean-dest");copyFolder(sourceFolder, destinationFolder);}


private static void copyFolder(File sourceFolder, File destinationFolder) throws IOException{    //如果是dir则递归遍历创建dir,如果是文件则直接拷贝    if (sourceFolder.isDirectory())    {        //查看目标dir是否存在        if (!destinationFolder.exists())        {            destinationFolder.mkdir();            log.info("目标dir已经创建: {}",destinationFolder);        }        for (String file : sourceFolder.list())        {            File srcFile = new File(sourceFolder, file);            File destFile = new File(destinationFolder, file);            copyFolder(srcFile, destFile);        }    }    else    {        //使用Files.copy来拷贝具体的文件        Files.copy(sourceFolder.toPath(), destinationFolder.toPath(), StandardCopyOption.REPLACE_EXISTING);        log.info("拷贝目标文件: {}",destinationFolder);    }}
复制代码


基本思想就是遇到目录我就遍历,遇到文件我就拷贝。


目录的腰疼操作小师妹:F 师兄,假如我想删除一个目录中的文件,或者我们想统计一下这个目录下面到底有多少个文件该怎么做呢?


虽然这些操作有点腰疼,还是可以解决的,Files 工具类中有个方法叫做 walk,返回一个 Stream 对象,我们可以使用 Stream 的 API 来对文件进行处理。


删除文件:


public void useFileWalkToDelete() throws IOException {Path dir = Paths.get("src/main/resources/flydean");Files.walk(dir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);}统计文件:


public void useFileWalkToSumSize() throws IOException {


    Path folder = Paths.get("src/test/resources");    long size = Files.walk(folder)            .filter(p -> p.toFile().isFile())            .mapToLong(p -> p.toFile().length())            .sum();    log.info("dir size is: {}",size);}
复制代码


总结本文介绍了目录的一些非常常见和有用的操作。


第七章 文件系统和 WatchService 简介小师妹这次遇到了监控文件变化的问题,F 师兄给小师妹介绍了 JDK7 nio 中引入的 WatchService,没想到又顺道普及了一下文件系统的概念,万万没想到。


监控的痛点小师妹:F 师兄最近你有没有感觉到呼吸有点困难,后领有点凉飕飕的,说话有点不顺畅的那种?


没有啊小师妹,你是不是秋衣穿反了?


小师妹:不是的 F 师兄,我讲的是心里的感觉,那种莫须有的压力,还有一丝悸动缠绕在心。


别绕弯子了小师妹,是不是又遇到问题了。


更多内容请访问 www.flydean.com


小师妹:还是 F 师兄懂我,这不上次的 Properties 文件用得非常上手,每次修改 Properties 文件都要重启 java 应用程序,真的是很痛苦。有没有什么其他的办法呢?


办法当然有,最基础的办法就是开一个线程定时去监控属性文件的最后修改时间,如果修改了就重新加载,这样不就行了。


小师妹:写线程啊,这么麻烦,有没有什么更简单的办法呢?


就知道你要这样问,还好我准备的比较充分,今天给你介绍一个 JDK7 在 nio 中引入的类 WatchService。


WatchService 和文件系统 WatchService 是 JDK7 在 nio 中引入的接口:


监控的服务叫做 WatchService,被监控的对象叫做 Watchable:


WatchKey register(WatchService watcher,WatchEvent.Kind<?>[] events,WatchEvent.Modifier... modifiers)throws IOException;WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events)throws IOException;Watchable 通过 register 将该对象的 WatchEvent 注册到 WatchService 上。从此只要有 WatchEvent 发生在 Watchable 对象上,就会通知 WatchService。


WatchEvent 有四种类型:


ENTRY_CREATE 目标被创建 ENTRY_DELETE 目标被删除 ENTRY_MODIFY 目标被修改 OVERFLOW 一个特殊的 Event,表示 Event 被放弃或者丢失 register 返回的 WatchKey 就是监听到的 WatchEvent 的集合。


现在来看 WatchService 的 4 个方法:


close 关闭 watchServicepoll 获取下一个 watchKey,如果没有则返回 null 带时间参数的 poll 在等待的一定时间内获取下一个 watchKeytake 获取下一个 watchKey,如果没有则一直等待小师妹:F 师兄,那怎么才能构建一个 WatchService 呢?


上次文章中说的文件系统,小师妹还记得吧,FileSystem 中就有一个获取 WatchService 的方法:


public abstract WatchService newWatchService() throws IOException;我们看下 FileSystem 的结构图:


在我的 mac 系统上,FileSystem 可以分为三大类,UnixFileSystem,JrtFileSystem 和 ZipFileSystem。我猜在 windows 上面应该还有对应的 windows 相关的文件系统。小师妹你要是有兴趣可以去看一下。


小师妹:UnixFileSystem 用来处理 Unix 下面的文件,ZipFileSystem 用来处理 zip 文件。那 JrtFileSystem 是用来做什么的?


哎呀,这就又要扯远了,为什么每次问问题都要扯到天边….


从前当 JDK 还是 9 的时候,做了一个非常大的改动叫做模块化 JPMS(Java Platform Module System),这个 Jrt 就是为了给模块化系统用的,我们来举个例子:


public void useJRTFileSystem(){String resource = "java/lang/Object.class";URL url = ClassLoader.getSystemResource(resource);log.info("{}",url);}上面一段代码我们获取到了 Object 这个 class 的 url,我们看下如果是在 JDK8 中,输出是什么:


jar:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/lib/rt.jar!/java/lang/Object.class 输出结果是 jar:file 表示这个 Object class 是放在 jar 文件中的,后面是 jar 文件的路径。


如果是在 JDK9 之后:


jrt:/java.base/java/lang/Object.class 结果是 jrt 开头的,java.base 是模块的名字,后面是 Object 的路径。看起来是不是比传统的 jar 路径更加简洁明了。


有了文件系统,我们就可以在获取系统默认的文件系统的同时,获取到相应的 WatchService:


WatchService watchService = FileSystems.getDefault().newWatchService();WatchSerice 的使用和实现本质小师妹:F 师兄,WatchSerice 是咋实现的呀?这么神奇,为我们省了这么多工作。


其实 JDK 提供了这么多类的目的就是为了不让我们重复造轮子,之前跟你讲监控文件的最简单办法就是开一个独立的线程来监控文件变化吗?其实…..WatchService 就是这样做的!


PollingWatchService() {// TBD: Make the number of threads configurablescheduledExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(null, r, "FileSystemWatcher", 0, false);t.setDaemon(true);return t;}});}上面的方法就是生成 WatchService 的方法,小师妹看到没有,它的本质就是开启了一个 daemon 的线程,用来接收监控任务。


下面看下怎么把一个文件注册到 WatchService 上面:


private void startWatcher(String dirPath, String file) throws IOException {WatchService watchService = FileSystems.getDefault().newWatchService();Path path = Paths.get(dirPath);path.register(watchService, ENTRY_MODIFY);


    Runtime.getRuntime().addShutdownHook(new Thread(() -> {        try {            watchService.close();        } catch (IOException e) {            log.error(e.getMessage());        }    }));
WatchKey key = null; while (true) { try { key = watchService.take(); for (WatchEvent<?> event : key.pollEvents()) { if (event.context().toString().equals(fileName)) { loadConfig(dirPath + file); } } boolean reset = key.reset(); if (!reset) { log.info("该文件无法重置"); break; } } catch (Exception e) { log.error(e.getMessage()); } }}
复制代码


上面的关键方法就是 path.register,其中 Path 是一个 Watchable 对象。


然后使用 watchService.take 来获取生成的 WatchEvent,最后根据 WatchEvent 来处理文件。


总结道生一,一生二,二生三,三生万物。一个简简单单的功能其实背后隐藏着…道德经,哦,不对,背后隐藏着道的哲学。


第八章 文件 File 和路径 Path 简介文件和路径有什么关系?文件和路径又隐藏了什么秘密?在文件系统的管理下,创建路径的方式又有哪些?今天 F 师兄带小师妹再给大家来一场精彩的表演。


文件和路径小师妹:F 师兄我有一个问题,java 中的文件 File 是一个类可以理解,因为文件里面包含了很多其他的信息,但是路径 Path 为什么也要单独一个类出来?只用一个 String 表示不是更简单?


更多内容请访问 www.flydean.com


万物皆有因,没有无缘无故的爱,也没有无缘无故的恨。一切真的是妙不可言啊。


我们来看下 File 和 path 的定义:


public class Fileimplements Serializable, Comparable<File>public interface Pathextends Comparable<Path>, Iterable<Path>, Watchable 首先,File 是一个类,它表示的是所有的文件系统都拥有的属性和功能,不管你是 windows 还是 linux,他们中的 File 对象都应该是一样的。


File 中包含了 Path,小师妹你且看,Path 是一个 interface,为什么是一个 interface 呢?因为 Path 根据不同的情况可以分为 JrtPath,UnixPath 和 ZipPath。三个 Path 所对应的 FileSystem 我们在上一篇文章中已经讨论过了。所以 Path 的实现是不同的,但是包含 Path 的 File 是相同的。


小师妹:F 师兄,这个怎么这么拗口,给我来一个直白通俗的解释吧。


既然这样,且听我解释:爱国版的,或许我们属于不同的民族,但是我们都是中国人。通俗版的,大家都是文化人儿,为啥就你这么拽。文化版的,同九年,汝何秀?


再看两者的实现接口,File 实现了 Serializable 表示可以被序列化,实现了 Comparable,表示可以被排序。


Path 继承 Comparable,表示可以被排序。继承 Iterable 表示可以被遍历,可以被遍历是因为 Path 可以表示目录。继承 Watchable,表示可以被注册到 WatchService 中,进行监控。


文件中的不同路径小师妹:F 师兄,File 中有好几个关于 Path 的 get 方法,能讲一下他们的不同之处吗?


直接上代码:


public void getFilePath() throws IOException {File file= new File("../../www.flydean.com.txt");log.info("name is : {}",file.getName());


    log.info("path is : {}",file.getPath());    log.info("absolutePath is : {}",file.getAbsolutePath());    log.info("canonicalPath is : {}",file.getCanonicalPath());}
复制代码


File 中有三个跟 Path 有关的方法,分别是 getPath,getAbsolutePath 和 getCanonicalPath。


getPath 返回的结果就是 new File 的时候传入的路径,输入什么返回什么。


getAbsolutePath 返回的是绝对路径,就是在 getPath 前面加上了当前的路径。


getCanonicalPath 返回的是精简后的 AbsolutePath,就是去掉了.或者..之类的指代符号。


看下输出结果:


INFO com.flydean.FilePathUsage - name is : www.flydean.com.txtINFO com.flydean.FilePathUsage - path is : ../../www.flydean.com.txtINFO com.flydean.FilePathUsage - absolutePath is : /Users/flydean/learn-java-io-nio/file-path/../../www.flydean.com.txtINFO com.flydean.FilePathUsage - canonicalPath is : /Users/flydean/www.flydean.com.txt 构建不同的 Path 小师妹:F 师兄,我记得路径有相对路径,绝对路径等,是不是也有相应的创建 Path 的方法呢?


当然有的,先看下绝对路径的创建:


public void getAbsolutePath(){Path absolutePath = Paths.get("/data/flydean/learn-java-io-nio/file-path", "src/resource","www.flydean.com.txt");log.info("absolutePath {}",absolutePath );}我们可以使用 Paths.get 方法传入绝对路径的地址来构建绝对路径。


同样使用 Paths.get 方法,传入非绝对路径可以构建相对路径。


public void getRelativePath(){Path RelativePath = Paths.get("src", "resource","www.flydean.com.txt");log.info("absolutePath {}",RelativePath.toAbsolutePath() );}我们还可以从 URI 中构建 Path:


public void getPathfromURI(){URI uri = URI.create("file:///data/flydean/learn-java-io-nio/file-path/src/resource/www.flydean.com.txt");log.info("schema {}",uri.getScheme());log.info("default provider absolutePath {}",FileSystems.getDefault().provider().getPath(uri).toAbsolutePath().toString());}也可以从 FileSystem 构建 Path:


public void getPathWithFileSystem(){Path path1 = FileSystems.getDefault().getPath(System.getProperty("user.home"), "flydean", "flydean.txt");log.info(path1.toAbsolutePath().toString());


        Path path2 = FileSystems.getDefault().getPath("/Users", "flydean", "flydean.txt");        log.info(path2.toAbsolutePath().toString());
}
复制代码


总结好多好多 Path 的创建方法,总有一款适合你。快来挑选吧。


第九章 Buffer 和 Buff 简介小师妹在学习 NIO 的路上越走越远,唯一能够帮到她的就是在她需要的时候给她以全力的支持。什么都不说了,今天介绍的是 NIO 的基础 Buffer。老铁给我上个 Buff。


Buffer 是什么小师妹:F 师兄,这个 Buffer 是我们纵横王者峡谷中那句:老铁给我加个 Buff 的意思吗?


当然不是了,此 Buffer 非彼 Buff,Buffer 是 NIO 的基础,没有 Buffer 就没有 NIO,没有 Buffer 就没有今天的 java。


因为 NIO 是按 Block 来读取数据的,这个一个 Block 就可以看做是一个 Buffer。我们在 Buffer 中存储要读取的数据和要写入的数据,通过 Buffer 来提高读取和写入的效率。


更多内容请访问 www.flydean.com


还记得 java 对象的底层存储单位是什么吗?


小师妹:这个我知道,java 对象的底层存储单位是字节 Byte。


对,我们看下 Buffer 的继承图:


Buffer 是一个接口,它下面有诸多实现,包括最基本的 ByteBuffer 和其他的基本类型封装的其他 Buffer。


小师妹:F 师兄,有 ByteBuffer 不就够了吗?还要其他的类型 Buffer 做什么?


小师妹,山珍再好,也有吃腻的时候,偶尔也要换个萝卜白菜啥的,你以为乾隆下江南都干了些啥?


ByteBuffer 虽然好用,但是它毕竟是最小的单位,在它之上我们还有 Char,int,Double,Short 等等基础类型,为了简单起见,我们也给他们都搞一套 Buffer。


Buffer 进阶小师妹:F 师兄,既然 Buffer 是这些基础类型的集合,为什么不直接用结合来表示呢?给他们封装成一个对象,好像有点多余。


我们既然在面向对象的世界,从表面来看自然是使用 Object 比较合乎情理,从底层的本质上看,这些封装的 Buffer 包含了一些额外的元数据信息,并且还提供了一些意想不到的功能。


上图列出了 Buffer 中的几个关键的概念,分别是 Capacity,Limit,Position 和 Mark。Buffer 底层的本质是数组,我们以 ByteBuffer 为例,它的底层是:


final byte[] hb;Capacity 表示的是该 Buffer 能够承载元素的最大数目,这个是在 Buffer 创建初期就设置的,不可以被改变。Limit 表示的 Buffer 中可以被访问的元素个数,也就是说 Buffer 中存活的元素个数。Position 表示的是下一个可以被访问元素的 index,可以通过 put 和 get 方法进行自动更新。Mark 表示的是历史 index,当我们调用 mark 方法的时候,会把设置 Mark 为当前的 position,通过调用 reset 方法把 Mark 的值恢复到 position 中。创建 Buffer 小师妹:F 师兄呀,这么多 Buffer 创建起来是不是很麻烦?有没有什么快捷的使用办法?


一般来说创建 Buffer 有两种方法,一种叫做 allocate,一种叫做 wrap。


public void createBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);log.info("{}",intBuffer);log.info("{}",intBuffer.hasArray());int[] intArray=new int[10];IntBuffer intBuffer2= IntBuffer.wrap(intArray);log.info("{}",intBuffer2);IntBuffer intBuffer3= IntBuffer.wrap(intArray,2,5);log.info("{}",intBuffer3);intBuffer3.clear();log.info("{}",intBuffer3);log.info("{}",intBuffer3.hasArray());}allocate 可以为 Buffer 分配一个空间,wrap 同样为 Buffer 分配一个空间,不同的是这个空间背后的数组是自定义的,wrap 还支持三个参数的方法,后面两个参数分别是 offset 和 length。


INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]INFO com.flydean.BufferUsage - trueINFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=2 lim=7 cap=10]INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]INFO com.flydean.BufferUsage - truehasArray 用来判断该 Buffer 的底层是不是数组实现的,可以看到,不管是 wrap 还是 allocate,其底层都是数组。


需要注意的一点,最后,我们调用了 clear 方法,clear 方法调用之后,我们发现 Buffer 的 position 和 limit 都被重置了。这说明 wrap 的三个参数方法设定的只是初始值,可以被重置。


Direct VS non-Direct 小师妹:F 师兄,你说了两种创建 Buffer 的方法,但是两种 Buffer 的后台都是数组,难道还有非数组的 Buffer 吗?


自然是有的,但是只有 ByteBuffer 有。ByteBuffer 有一个 allocateDirect 方法,可以分配 Direct Buffer。


小师妹:Direct 和非 Direct 有什么区别呢?


Direct Buffer 就是说,不需要在用户空间再复制拷贝一份数据,直接在虚拟地址映射空间中进行操作。这叫 Direct。这样做的好处就是快。缺点就是在分配和销毁的时候会占用更多的资源,并且因为 Direct Buffer 不在用户空间之内,所以也不受垃圾回收机制的管辖。


所以通常来说只有在数据量比较大,生命周期比较长的数据来使用 Direct Buffer。


看下代码:


public void createByteBuffer() throws IOException {ByteBuffer byteBuffer= ByteBuffer.allocateDirect(10);log.info("{}",byteBuffer);log.info("{}",byteBuffer.hasArray());log.info("{}",byteBuffer.isDirect());


    try (RandomAccessFile aFile = new RandomAccessFile("src/main/resources/www.flydean.com", "r");         FileChannel inChannel = aFile.getChannel()) {        MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());        log.info("{}",buffer);        log.info("{}",buffer.hasArray());        log.info("{}",buffer.isDirect());    }}
复制代码


除了 allocateDirect,使用 FileChannel 的 map 方法也可以得到一个 Direct 的 MappedByteBuffer。


上面的例子输出结果:


INFO com.flydean.BufferUsage - java.nio.DirectByteBuffer[pos=0 lim=10 cap=10]INFO com.flydean.BufferUsage - falseINFO com.flydean.BufferUsage - trueINFO com.flydean.BufferUsage - java.nio.DirectByteBufferR[pos=0 lim=0 cap=0]INFO com.flydean.BufferUsage - falseINFO com.flydean.BufferUsage - trueBuffer 的日常操作小师妹:F 师兄,看起来 Buffer 确实有那么一点复杂,那么 Buffer 都有哪些操作呢?


Buffer 的操作有很多,下面我们一一来讲解。


向 Buffer 写数据向 Buffer 写数据可以调用 Buffer 的 put 方法:


public void putBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);intBuffer.put(1).put(2).put(3);log.info("{}",intBuffer.array());intBuffer.put(0,4);log.info("{}",intBuffer.array());}因为 put 方法返回的还是一个 IntBuffer 类,所以 Buffer 的 put 方法可以像 Stream 那样连写。


同时,我们还可以指定 put 在什么位置。上面的代码输出:


INFO com.flydean.BufferUsage - [1, 2, 3, 0, 0, 0, 0, 0, 0, 0]INFO com.flydean.BufferUsage - [4, 2, 3, 0, 0, 0, 0, 0, 0, 0]从 Buffer 读数据读数据使用 get 方法,但是在 get 方法之前我们需要调用 flip 方法。


flip 方法是做什么用的呢?上面讲到 Buffer 有个 position 和 limit 字段,position 会随着 get 或者 put 的方法自动指向后面一个元素,而 limit 表示的是该 Buffer 中有多少可用元素。


如果我们要读取 Buffer 的值则会从 positon 开始到 limit 结束:


public void getBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);intBuffer.put(1).put(2).put(3);intBuffer.flip();while (intBuffer.hasRemaining()) {log.info("{}",intBuffer.get());}intBuffer.clear();}可以通过 hasRemaining 来判断是否还有下一个元素。通过调用 clear 来清除 Buffer,以供下次使用。


rewind Bufferrewind 和 flip 很类似,不同之处在于 rewind 不会改变 limit 的值,只会将 position 重置为 0。


public void rewindBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);intBuffer.put(1).put(2).put(3);log.info("{}",intBuffer);intBuffer.rewind();log.info("{}",intBuffer);}上面的结果输出:


INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]Compact BufferBuffer 还有一个 compact 方法,顾名思义 compact 就是压缩的意思,就是把 Buffer 从当前 position 到 limit 的值赋值到 position 为 0 的位置:


public void useCompact(){IntBuffer intBuffer= IntBuffer.allocate(10);intBuffer.put(1).put(2).put(3);intBuffer.flip();log.info("{}",intBuffer);intBuffer.get();intBuffer.compact();log.info("{}",intBuffer);log.info("{}",intBuffer.array());}上面代码输出:


INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=2 lim=10 cap=10]INFO com.flydean.BufferUsage - [2, 3, 3, 0, 0, 0, 0, 0, 0, 0]duplicate Buffer 最后我们讲一下复制 Buffer,有三种方法,duplicate,asReadOnlyBuffer,和 slice。


duplicate 就是拷贝原 Buffer 的 position,limit 和 mark,它和原 Buffer 是共享原始数据的。所以修改了 duplicate 之后的 Buffer 也会同时修改原 Buffer。


如果用 asReadOnlyBuffer 就不允许拷贝之后的 Buffer 进行修改。


slice 也是 readOnly 的,不过它拷贝的是从原 Buffer 的 position 到 limit-position 之间的部分。


public void duplicateBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);intBuffer.put(1).put(2).put(3);log.info("{}",intBuffer);IntBuffer duplicateBuffer=intBuffer.duplicate();log.info("{}",duplicateBuffer);IntBuffer readOnlyBuffer=intBuffer.asReadOnlyBuffer();log.info("{}",readOnlyBuffer);IntBuffer sliceBuffer=intBuffer.slice();log.info("{}",sliceBuffer);}输出结果:


INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]INFO com.flydean.BufferUsage - java.nio.HeapIntBufferR[pos=3 lim=10 cap=10]INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=7 cap=7]总结今天给小师妹介绍了 Buffer 的原理和基本操作。


第十章 File copy 和 File filter 简介一个 linux 命令的事情,小师妹非要让我教她怎么用 java 来实现,哎,摊上个这么杠精的小师妹,我也是深感无力,做一个师兄真的好难。


使用 java 拷贝文件今天小师妹找到我了:F 师兄,能告诉怎么拷贝文件吗?


拷贝文件?不是很简单的事情吗?如果你有了文件的读权限,只需要这样就可以了。


cp www.flydean.com www.flydean.com.back 当然,如果是目录的话还可以加两个参数遍历和强制拷贝:


cp -rf srcDir distDir 这么简单的 linux 命令,不要告诉我你不会。


小师妹笑了:F 师兄,我不要用 linux 命令,我就想用 java 来实现,我不正在学 java 吗?学一门当然要找准机会来练习啦,快快教教我吧。


既然这样,那我就开讲了。java 中文件的拷贝其实也有三种方法,可以使用传统的文件读写的方法,也可以使用最新的 NIO 中提供的拷贝方法。


使用传统方法当然没有 NIO 快,也没有 NIO 简洁,我们先来看看怎么使用传统的文件读写的方法来拷贝文件:


public  void copyWithFileStreams() throws IOException{    File fileToCopy = new File("src/main/resources/www.flydean.com");    File newFile = new File("src/main/resources/www.flydean.com.back");    newFile.createNewFile();    try(FileOutputStream output = new FileOutputStream(newFile);FileInputStream input = new FileInputStream(fileToCopy)){        byte[] buf = new byte[1024];        int bytesRead;        while ((bytesRead = input.read(buf)) > 0)        {            output.write(buf, 0, bytesRead);        }    }}
复制代码


上面的例子中,我们首先定义了两个文件,然后从两个文件中生成了 OutputStream 和 InputStream,最后以字节流的形式从 input 中读出数据到 outputStream 中,最终完成了文件的拷贝。


传统的 File IO 拷贝比较繁琐,速度也比较慢。我们接下来看看怎么使用 NIO 来完成这个过程:


public void copyWithNIOChannel() throws IOException{File fileToCopy = new File("src/main/resources/www.flydean.com");File newFile = new File("src/main/resources/www.flydean.com.back");


    try(FileInputStream inputStream = new FileInputStream(fileToCopy);FileOutputStream outputStream = new FileOutputStream(newFile)){        FileChannel inChannel = inputStream.getChannel();        FileChannel outChannel = outputStream.getChannel();        inChannel.transferTo(0, fileToCopy.length(), outChannel);    }}
复制代码


之前我们讲到 NIO 中一个非常重要的概念就是 channel,通过构建源文件和目标文件的 channel 通道,可以直接在 channel 层面进行拷贝,如上面的例子所示,我们调用了 inChannel.transferTo 完成了拷贝。


最后,还有一个更简单的 NIO 文件拷贝的方法:


public void copyWithNIOFiles() throws IOException{Path source = Paths.get("src/main/resources/www.flydean.com");Path destination = Paths.get("src/main/resources/www.flydean.com.back");Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);}直接使用工具类 Files 提供的 copy 方法即可。


使用 File filter 太棒了,小师妹一脸崇拜:F 师兄,我还有一个需求,就是想删除某个目录里面的以.log 结尾的日志文件,这个需求是不是很常见?F 师兄一般是怎么操作的?


一般这种操作我都是一个 linux 命令就搞定了,如果搞不定那就用两个:


rm -rf *.log 当然,如果需要,我们也是可以用 java 来实现的。


java 中提供了两个 Filter 都可以用来实现这个功能。


这两个 Filter 是 java.io.FilenameFilter 和 java.io.FileFilter:


@FunctionalInterfacepublic interface FilenameFilter {boolean accept(File dir, String name);}@FunctionalInterfacepublic interface FileFilter {boolean accept(File pathname);}这两个接口都是函数式接口,所以他们的实现可以直接用 lambda 表达式来代替。


两者的区别在于,FilenameFilter 进行过滤的是文件名和文件所在的目录。而 FileFilter 进行过滤的直接就是目标文件。


在 java 中是没有目录的概念的,一个目录也是用 File 的表示的。


上面的两个使用起来非常类似,我们就以 FilenameFilter 为例,看下怎么删除.log 文件:


public void useFileNameFilter(){String targetDirectory = "src/main/resources/";File directory = new File(targetDirectory);


    //Filter out all log files    String[] logFiles = directory.list( (dir, fileName)-> fileName.endsWith(".log"));
//If no log file found; no need to go further if (logFiles.length == 0) return;
//This code will delete all log files one by one for (String logfile : logFiles) { String tempLogFile = targetDirectory + File.separator + logfile; File fileDelete = new File(tempLogFile); boolean isdeleted = fileDelete.delete(); log.info("file : {} is deleted : {} ", tempLogFile , isdeleted); }}
复制代码


上面的例子中,我们通过 directory.list 方法,传入 lambda 表达式创建的 Filter,实现了过滤的效果。


最后,我们将过滤之后的文件删除。实现了目标。


总结小师妹的两个问题解决了,希望今天可以不要再见到她。


第十一章 NIO 中 Channel 的妙用简介小师妹,你还记得我们使用 IO 和 NIO 的初心吗?


小师妹:F 师兄,使用 IO 和 NIO 不就是为了让生活更美好,世界充满爱吗?让我等程序员可以优雅的将数据从一个地方搬运到另外一个地方。利其器,善其事,才有更多的时间去享受生活呀。


善,如果将数据比做人,IO,NIO 的目的就是把人运到美国。


小师妹:F 师兄,为什么要运到美国呀,美国现在新冠太严重了,还是待在中国吧。中国是世界上最安全的国家!


好吧,为了保险起见,我们要把人运到上海。人就是数据,怎么运过去呢?可以坐飞机,坐汽车,坐火车,这些什么飞机,汽车,火车就可以看做是一个一个的 Buffer。


最后飞机的航线,汽车的公路和火车的轨道就可以看做是一个个的 channel。


简单点讲,channel 就是负责运送 Buffer 的通道。


IO 按源头来分,可以分为两种,从文件来的 File IO,从 Stream 来的 Stream IO。不管哪种 IO,都可以通过 channel 来运送数据。


Channel 的分类虽然数据的来源只有两种,但是 JDK 中 Channel 的分类可不少,如下图所示:


先来看看最基本的,也是最顶层的接口 Channel:


public interface Channel extends Closeable {public boolean isOpen();public void close() throws IOException;


}最顶层的 Channel 很简单,继承了 Closeable 接口,需要实现两个方法 isOpen 和 close。


一个用来判断 channel 是否打开,一个用来关闭 channel。


小师妹:F 师兄,顶层的 Channel 怎么这么简单,完全不符合 Channel 很复杂的人设啊。


别急,JDK 这么做其实也是有道理的,因为是顶层的接口,必须要更加抽象更加通用,结果,一通用就发现还真的就只有这么两个方法是通用的。


所以为了应对这个问题,Channel 中定义了很多种不同的类型。


最最底层的 Channel 有 5 大类型,分别是:


FileChannel 这 5 大 channel 中,和文件 File 有关的就是这个 FileChannel 了。


FileChannel 可以从 RandomAccessFile, FileInputStream 或者 FileOutputStream 中通过调用 getChannel()来得到。


也可以直接调用 FileChannel 中的 open 方法传入 Path 创建。


public abstract class FileChannelextends AbstractInterruptibleChannelimplements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel 我们看下 FileChannel 继承或者实现的接口和类。


AbstractInterruptibleChannel 实现了 InterruptibleChannel 接口,interrupt 大家都知道吧,用来中断线程执行的利器。来看一下下面一段非常玄妙的代码:


protected final void begin() {if (interruptor == null) {interruptor = new Interruptible() {public void interrupt(Thread target) {synchronized (closeLock) {if (closed)return;closed = true;interrupted = target;try {AbstractInterruptibleChannel.this.implCloseChannel();} catch (IOException x) { }}}};}blockedOn(interruptor);Thread me = Thread.currentThread();if (me.isInterrupted())interruptor.interrupt(me);}上面这段代码就是 AbstractInterruptibleChannel 的核心所在。


首先定义了一个 Interruptible 的实例,这个实例中有一个 interrupt 方法,用来关闭 Channel。


然后获得当前线程的实例,判断当前线程是否 Interrupted,如果是的话,就调用 Interruptible 的 interrupt 方法将当前 channel 关闭。


SeekableByteChannel 用来连接 Entry 或者 File。它有一个独特的属性叫做 position,表示当前读取的位置。可以被修改。


GatheringByteChannel 和 ScatteringByteChannel 表示可以一次读写一个 Buffer 序列结合(Buffer Array):


public long write(ByteBuffer[] srcs, int offset, int length)throws IOException;public long read(ByteBuffer[] dsts, int offset, int length)throws IOException;Selector 和 Channel 在讲其他几个 Channel 之前,我们看一个和下面几个 channel 相关的 Selector:


这里要介绍一个新的 Channel 类型叫做 SelectableChannel,之前的 FileChannel 的连接是一对一的,也就是说一个 channel 要对应一个处理的线程。而 SelectableChannel 则是一对多的,也就是说一个处理线程可以通过 Selector 来对应处理多个 channel。


SelectableChannel 通过注册不同的 SelectionKey,实现对多个 Channel 的监听。后面我们会具体的讲解 Selector 的使用,敬请期待。


DatagramChannelDatagramChannel 是用来处理 UDP 的 Channel。它自带了 Open 方法来创建实例。


来看看 DatagramChannel 的定义:


public abstract class DatagramChannelextends AbstractSelectableChannelimplements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannelByteChannel 表示它同时是 ReadableByteChannel 也是 WritableByteChannel,可以同时写入和读取。


MulticastChannel 代表的是一种多播协议。正好和 UDP 对应。


SocketChannelSocketChannel 是用来处理 TCP 的 channel。它也是通过 Open 方法来创建的。


public abstract class SocketChannelextends AbstractSelectableChannelimplements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannelSocketChannel 跟 DatagramChannel 的唯一不同之处就是实现的是 NetworkChannel 借口。


NetworkChannel 提供了一些 network socket 的操作,比如绑定地址等。


ServerSocketChannelServerSocketChannel 也是一个 NetworkChannel,它主要用在服务器端的监听。


public abstract class ServerSocketChannelextends AbstractSelectableChannelimplements NetworkChannelAsynchronousSocketChannel 最后 AsynchronousSocketChannel 是一种异步的 Channel:


public abstract class AsynchronousSocketChannelimplements AsynchronousByteChannel, NetworkChannel 为什么是异步呢?我们看一个方法:


public abstract Future<Integer> read(ByteBuffer dst);可以看到返回值是一个 Future,所以 read 方法可以立刻返回,只在我们需要的时候从 Future 中取值即可。


使用 Channel 小师妹:F 师兄,讲了这么多种类的 Channel,看得我眼花缭乱,能不能讲一个 Channel 的具体例子呢?


好的小师妹,我们现在讲一个使用 Channel 进行文件拷贝的例子,虽然 Channel 提供了 transferTo 的方法可以非常简单的进行拷贝,但是为了能够看清楚 Channel 的通用使用,我们选择一个更加常规的例子:


public void useChannelCopy() throws IOException {FileInputStream input = new FileInputStream ("src/main/resources/www.flydean.com");FileOutputStream output = new FileOutputStream ("src/main/resources/www.flydean.com.txt");try(ReadableByteChannel source = input.getChannel(); WritableByteChannel dest = output.getChannel()){ByteBuffer buffer = ByteBuffer.allocateDirect(1024);while (source.read(buffer) != -1){// flip buffer,准备写入 buffer.flip();// 查看是否有更多的内容 while (buffer.hasRemaining()){dest.write(buffer);}// clear buffer,供下一次使用 buffer.clear();}}}上面的例子中我们从 InputStream 中读取 Buffer,然后写入到 FileOutputStream。


总结今天讲解了 Channel 的具体分类,和一个简单的例子,后面我们会再体验一下 Channel 的其他例子,敬请期待。


第十二章 MappedByteBuffer 多大的文件我都装得下简介大大大,我要大!小师妹要读取的文件越来越大,该怎么帮帮她,让程序在性能和速度上面得到平衡呢?快来跟 F 师兄一起看看吧。


虚拟地址空间小师妹:F 师兄,你有没有发现,最近硬盘的价格真的是好便宜好便宜,1T 的硬盘大概要 500 块,平均 1M 五毛钱。现在下个电影都 1G 起步,这是不是意味着我们买入了大数据时代?


没错,小师妹,硬件技术的进步也带来了软件技术的进步,两者相辅相成,缺一不可。


小师妹:F 师兄,如果要是去读取 G 级的文件,有没有什么快捷简单的方法?


还记得上次我们讲的虚拟地址空间吗?


再把上次讲的图搬过来:


通常来说我们的应用程序调用系统的接口从磁盘空间获取 Buffer 数据,我们把自己的应用程序称之为用户空间,把系统的底层称之为系统空间。


传统的 IO 操作,是操作系统讲磁盘中的文件读入到系统空间里面,然后再拷贝到用户空间中,供用户使用。


这中间多了一个 Buffer 拷贝的过程,如果这个量够大的话,其实还是挺浪费时间的。


于是有人在想了,拷贝太麻烦太耗时了,我们单独划出一块内存区域,让系统空间和用户空间同时映射到同一块地址不就省略了拷贝的步骤吗?


这个被划出来的单独的内存区域叫做虚拟地址空间,而不同空间到虚拟地址的映射就叫做 Buffer Map。 Java 中是有一个专门的 MappedByteBuffer 来代表这种操作。


小师妹:F 师兄,那这个虚拟地址空间和内存有什么区别呢?有了内存还要啥虚拟地址空间?


虚拟地址空间有两个好处。


第一个好处就是虚拟地址空间对于应用程序本身而言是独立的,从而保证了程序的互相隔离和程序中地址的确定性。比如说一个程序如果运行在虚拟地址空间中,那么它的空间地址是固定的,不管他运行多少次。如果直接使用内存地址,那么可能这次运行的时候内存地址可用,下次运行的时候内存地址不可用,就会导致潜在的程序出错。


第二个好处就是虚拟空间地址可以比真实的内存地址大,这个大其实是对内存的使用做了优化,比如说会把很少使用的内存写如磁盘,从而释放出更多的内存来做更有意义的事情,而之前存储到磁盘的数据,当真正需要的时候,再从磁盘中加载到内存中。


这样物理内存实际上可以看做虚拟空间地址的缓存。


详解 MappedByteBuffer 小师妹:MappedByteBuffer 听起来好神奇,怎么使用它呢?


我们先来看看 MappedByteBuffer 的定义:


public abstract class MappedByteBufferextends ByteBuffer 它实际上是一个抽象类,具体的实现有两个:


class DirectByteBuffer extends MappedByteBuffer implements DirectBufferclass DirectByteBufferR extends DirectByteBufferimplements DirectBuffer 分别是 DirectByteBuffer 和 DirectByteBufferR。


小师妹:F 师兄,这两个 ByteBuffer 有什么区别呢?这个 R 是什么意思?


R 代表的是 ReadOnly 的意思,可能是因为本身是个类的名字就够长了,所以搞了个缩写。但是也不写个注解,让人看起来十分费解….


我们可以从 RandomAccessFile 的 FilChannel 中调用 map 方法获得它的实例。


我们看下 map 方法的定义:


public abstract MappedByteBuffer map(MapMode mode, long position, long size)throws IOException;MapMode 代表的是映射的模式,position 表示是 map 开始的地址,size 表示是 ByteBuffer 的大小。


MapMode 小师妹:F 师兄,文件有只读,读写两种模式,是不是 MapMode 也包含这两类?


对的,其实 NIO 中的 MapMode 除了这两个之外,还有一些其他很有趣的用法。


FileChannel.MapMode.READ_ONLY 表示只读模式 FileChannel.MapMode.READ_WRITE 表示读写模式 FileChannel.MapMode.PRIVATE 表示 copy-on-write 模式,这个模式和 READ_ONLY 有点相似,它的操作是先对原数据进行拷贝,然后可以在拷贝之后的 Buffer 中进行读写。但是这个写入并不会影响原数据。可以看做是数据的本地拷贝,所以叫做 Private。基本的 MapMode 就这三种了,其实除了基础的 MapMode,还有两种扩展的 MapMode:


ExtendedMapMode.READ_ONLY_SYNC 同步的读 ExtendedMapMode.READ_WRITE_SYNC 同步的读写 MappedByteBuffer 的最大值小师妹:F 师兄,既然可以映射到虚拟内存空间,那么这个 MappedByteBuffer 是不是可以无限大?


当然不是了,首先虚拟地址空间的大小是有限制的,如果是 32 位的 CPU,那么一个指针占用的地址就是 4 个字节,那么能够表示的最大值是 0xFFFFFFFF,也就是 4G。


另外我们看下 map 方法中 size 的类型是 long,在 java 中 long 能够表示的最大值是 0x7fffffff,也就是 2147483647 字节,换算一下大概是 2G。也就是说 MappedByteBuffer 的最大值是 2G,一次最多只能 map 2G 的数据。


MappedByteBuffer 的使用小师妹,F 师兄我们来举两个使用 MappedByteBuffer 读写的例子吧。


善!


先看一下怎么使用 MappedByteBuffer 来读数据:


public void readWithMap() throws IOException {try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "r")){//get ChannelFileChannel fileChannel = file.getChannel();//get mappedByteBuffer from fileChannelMappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());// check bufferlog.info("is Loaded in physical memory: {}",buffer.isLoaded()); //只是一个提醒而不是 guaranteelog.info("capacity {}",buffer.capacity());//read the bufferfor (int i = 0; i < buffer.limit(); i++){log.info("get {}", buffer.get());}}}然后再看一个使用 MappedByteBuffer 来写数据的例子:


public void writeWithMap() throws IOException {try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "rw")){//get ChannelFileChannel fileChannel = file.getChannel();//get mappedByteBuffer from fileChannelMappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8 );// check bufferlog.info("is Loaded in physical memory: {}",buffer.isLoaded()); //只是一个提醒而不是 guaranteelog.info("capacity {}",buffer.capacity());//write the contentbuffer.put("www.flydean.com".getBytes());}}MappedByteBuffer 要注意的事项小师妹:F 师兄,MappedByteBuffer 因为使用了内存映射,所以读写的速度都会有所提升。那么我们在使用中应该注意哪些问题呢?


MappedByteBuffer 是没有 close 方法的,即使它的 FileChannel 被 close 了,MappedByteBuffer 仍然处于打开状态,只有 JVM 进行垃圾回收的时候才会被关闭。而这个时间是不确定的。


总结本文再次介绍了虚拟地址空间和 MappedByteBuffer 的使用。


第十三章 NIO 中那些奇怪的 Buffer 简介妖魔鬼怪快快显形,今天 F 师兄帮助小师妹来斩妖除魔啦,什么 BufferB,BufferL,BufferRB,BufferRL,BufferS,BufferU,BufferRS,BufferRU 统统给你剖析个清清楚楚明明白白。


Buffer 的分类小师妹:F 师兄不都说 JDK 源码是最好的 java 老师吗?为程不识源码,就称牛人也枉然。但是我最近在学习 NIO 的时候竟然发现有些 Buffer 类居然没有注释,就那么突兀的写在哪里,让人好生心烦。


更多内容请访问 www.flydean.com


居然还有这样的事情?快带 F 师兄去看看。


小师妹:F 师兄你看,以 ShortBuffer 为例,它的子类怎么后面都带一些奇奇怪怪的字符:


什么什么 BufferB,BufferL,BufferRB,BufferRL,BufferS,BufferU,BufferRS,BufferRU 都来了,点进去看他们的源码也没有说明这些类到底是做什么的。


还真有这种事情,给我一个小时,让我仔细研究研究。


一个小时后,小师妹,经过我一个小时的辛苦勘察,结果发现,确实没有官方文档介绍这几个类到底是什么含义,但是师兄我掐指一算,好像发现了这些类之间的小秘密,且听为兄娓娓道来。


之前的文章,我们讲到 Buffer 根据类型可以分为 ShortBuffer,LongBuffer,DoubleBuffer 等等。


但是根据本质和使用习惯,我们又可以分为三类,分别是:ByteBufferAsXXXBuffer,DirectXXXBuffer 和 HeapXXXBuffer。


ByteBufferAsXXXBuffer 主要将 ByteBuffer 转换成为特定类型的 Buffer,比如 CharBuffer,IntBuffer 等等。


而 DirectXXXBuffer 则是和虚拟内存映射打交道的 Buffer。


最后 HeapXXXBuffer 是在堆空间上面创建的 Buffer。


Big Endian 和 Little Endian 小师妹,F 师兄,你刚刚讲的都不重要,我就想知道类后面的 B,L,R,S,U 是做什么的。


好吧,在给你讲解这些内容之前,师兄我给你讲一个故事。


话说在明末浙江才女吴绛雪写过一首诗:《春 景 诗》


莺啼岸柳弄春晴,柳弄春晴夜月明。明月夜晴春弄柳,晴春弄柳岸啼莺。


小师妹,可有看出什么特异之处?最好是多读几遍,读出声来。


小师妹:哇,F 师兄,这首诗从头到尾和从尾到头读起来是一样的呀,又对称又有意境!


不错,这就是中文的魅力啦,根据读的方式不同,得出的结果也不同,其实在计算机世界也存在这样的问题。


我们知道在 java 中底层的最小存储单元是 Byte,一个 Byte 是 8bits,用 16 进制表示就是 Ox00-OxFF。


java 中除了 byte,boolean 是占一个字节以外,好像其他的类型都会占用多个字节。


如果以 int 来举例,int 占用 4 个字节,其范围是从 Ox00000000-OxFFFFFFFF,假如我们有一个 int=Ox12345678,存到内存地址里面就有这样两种方式。


第一种 Big Endian 将高位的字节存储在起始地址


第二种 Little Endian 将地位的字节存储在起始地址


其实 Big Endian 更加符合人类的读写习惯,而 Little Endian 更加符合机器的读写习惯。


目前主流的两大 CPU 阵营中,PowerPC 系列采用 big endian 方式存储数据,而 x86 系列则采用 little endian 方式存储数据。


如果不同的 CPU 架构直接进行通信,就由可能因为读取顺序的不同而产生问题。


java 的设计初衷就是一次编写处处运行,所以自然也做了设计。


所以 BufferB 表示的是 Big Endian 的 buffer,BufferL 表示的是 Little endian 的 Buffer。


而 BufferRB,BufferRL 表示的是两种只读 Buffer。


aligned 内存对齐小师妹:F 师兄,那这几个又是做什么用的呢? BufferS,BufferU,BufferRS,BufferRU。


在讲解这几个类之前,我们先要回顾一下 JVM 中对象的存储方式。


还记得我们是怎么使用 JOL 来分析 JVM 的信息的吗?代码非常非常简单:


log.info("{}", VM.current().details());输出结果:

Running 64-bit HotSpot VM.

Using compressed oop with 3-bit shift.

Using compressed klass with 3-bit shift.

WARNING | Compressed references base/shifts are guessed by the experiment!

WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.

WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.

Objects are 8 bytes aligned.

Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

上面的输出中,我们可以看到:Objects are 8 bytes aligned,这意味着所有的对象分配的字节都是 8 的整数倍。


再注意上面输出的一个关键字 aligned,确认过眼神,是对的那个人。


aligned 对齐的意思,表示 JVM 中的对象都是以 8 字节对齐的,如果对象本身占用的空间不足 8 字节或者不是 8 字节的倍数,则补齐。


还是用 JOL 来分析 String 对象:


[main] INFO com.flydean.JolUsage - java.lang.String object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 12 (object header) N/A12 4 byte[] String.value N/A16 4 int String.hash N/A20 1 byte String.coder N/A21 1 boolean String.hashIsZero N/A22 2 (loss due to the next object alignment)Instance size: 24 bytesSpace losses: 0 bytes internal + 2 bytes external = 2 bytes total 可以看到一个 String 对象占用 24 字节,但是真正有意义的是 22 字节,有两个 2 字节是补齐用的。


对齐的好处显而易见,就是 CPU 在读取数据的时候更加方便和快捷,因为 CPU 设定是一次读取多少字节来的,如果你存储是没有对齐的,则 CPU 读取起来效率会比较低。


现在可以回答部分问题:BufferU 表示是 unaligned,BufferRU 表示是只读的 unaligned。


小师妹:那 BufferS 和 BufferRS 呢?


这个问题其实还是很难回答的,但是经过师兄我的不断研究和探索,终于找到了答案:


先看下 DirectShortBufferRU 和 DirectShortBufferRS 的区别,两者的区别在两个地方,先看第一个 Order:


DirectShortBufferRU:


public ByteOrder order() {return ((ByteOrder.nativeOrder() != ByteOrder.BIG_ENDIAN)? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);}DirectShortBufferRS:


public ByteOrder order() {return ((ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN)? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);}可以看到 DirectShortBufferRU 的 Order 是跟 nativeOrder 是一致的。而 DirectShortBufferRS 的 Order 跟 nativeOrder 是相反的。


为什么相反?再看两者 get 方法的不同:


DirectShortBufferU:


public short get() {try {checkSegment();return ((UNSAFE.getShort(ix(nextGetIndex()))));} finally {Reference.reachabilityFence(this);}}DirectShortBufferS:


public short get() {try {checkSegment();return (Bits.swap(UNSAFE.getShort(ix(nextGetIndex()))));} finally {Reference.reachabilityFence(this);}}区别出来了,DirectShortBufferS 在返回的时候做了一个 bits 的 swap 操作。


所以 BufferS 表示的是 swap 过后的 Buffer,和 BufferRS 表示的是只读的 swap 过后的 Buffer。


总结不写注释实在是害死人啊!尤其是 JDK 自己也不写注释的情况下!


第十四章 用 Selector 来说再见简介 NIO 有三宝:Buffer,Channel,Selector 少不了。本文将会介绍 NIO 三件套中的最后一套 Selector,并在理解 Selector 的基础上,协助小师妹发一张好人卡。我们开始吧。


Selector 介绍小师妹:F 师兄,最近我的桃花有点旺,好几个师兄莫名其妙的跟我打招呼,可是我一心向着工作,不想谈论这些事情。毕竟先有事业才有家嘛。我又不好直接拒绝,有没有什么比较隐晦的方法来让他们放弃这个想法?


更多内容请访问 www.flydean.com


这个问题,我沉思了大约 0.001 秒,于是给出了答案:给他们发张好人卡吧,应该就不会再来纠缠你了。


小师妹:F 师兄,如果给他们发完好人卡还没有用呢?


那就只能切断跟他们的联系了,来个一刀两断。哈哈。


这样吧,小师妹你最近不是在学 NIO 吗?刚好我们可以用 Selector 来模拟一下发好人卡的过程。


假如你的志伟师兄和子丹师兄想跟你建立联系,每个人都想跟你建立一个沟通通道,那么你就需要创建两个 channel。


两个 channel 其实还好,如果有多个人都想同时跟你建立联系通道,那么要维持这些通道就需要保持连接,从而浪费了资源。


但是建立的这些连接并不是时时刻刻都有消息在传输,所以其实大多数时间这些建立联系的通道其实是浪费的。


如果使用 Selector 就可以只启用一个线程来监听通道的消息变动,这就是 Selector。


从上面的图可以看出,Selector 监听三个不同的 channel,然后交给一个 processor 来处理,从而节约了资源。


创建 Selector 先看下 selector 的定义:


public abstract class Selector implements CloseableSelector 是一个 abstract 类,并且实现了 Closeable,表示 Selector 是可以被关闭的。


虽然 Selector 是一个 abstract 类,但是可以通过 open 来简单的创建:


Selector selector = Selector.open();如果细看 open 的实现可以发现一个很有趣的现象:


public static Selector open() throws IOException {return SelectorProvider.provider().openSelector();}open 方法调用的是 SelectorProvider 中的 openSelector 方法。


再看下 provider 的实现:


public SelectorProvider run() {if (loadProviderFromProperty())return provider;if (loadProviderAsService())return provider;provider = sun.nio.ch.DefaultSelectorProvider.create();return provider;}});有三种情况可以加载一个 SelectorProvider,如果系统属性指定了 java.nio.channels.spi.SelectorProvider,那么从指定的属性加载。


如果没有直接指定属性,则从 ServiceLoader 来加载。


最后如果都找不到的情况下,使用默认的 DefaultSelectorProvider。


关于 ServiceLoader 的用法,我们后面会有专门的文章来讲述。这里先不做多的解释。


注册 Selector 到 Channel 中 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));serverSocketChannel.configureBlocking(false);serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);如果是在服务器端,我们需要先创建一个 ServerSocketChannel,绑定 Server 的地址和端口,然后将 Blocking 设置为 false。因为我们使用了 Selector,它实际上是一个非阻塞的 IO。


注意 FileChannels 是不能使用 Selector 的,因为它是一个阻塞型 IO。


小师妹:F 师兄,为啥 FileChannel 是阻塞型的呀?做成非阻塞型的不是更快?


小师妹,我们使用 FileChannel 的目的是什么?就是为了读文件呀,读取文件肯定是一直读一直读,没有可能读一会这个 channel 再读另外一个 channel 吧,因为对于每个 channel 自己来讲,在文件没读取完之前,都是繁忙状态,没有必要在 channel 中切换。


最后我们将创建好的 Selector 注册到 channel 中去。


SelectionKeySelectionKey 表示的是我们希望监听到的事件。


总的来说,有 4 种 Event:


SelectionKey.OP_READ 表示服务器准备好,可以从 channel 中读取数据。SelectionKey.OP_WRITE 表示服务器准备好,可以向 channel 中写入数据。SelectionKey.OP_CONNECT 表示客户端尝试去连接服务端 SelectionKey.OP_ACCEPT 表示服务器 accept 一个客户端的请求 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;我们可以看到上面的 4 个 Event 是用位运算来定义的,如果将这个四个 event 使用或运算合并起来,就得到了 SelectionKey 中的 interestOps。


和 interestOps 类似,SelectionKey 还有一个 readyOps。


一个表示感兴趣的操作,一个表示 ready 的操作。


最后,SelectionKey 在注册的时候,还可以 attach 一个 Object,比如我们可以在这个对象中保存这个 channel 的 id:


SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT, object);key.attach(Object);Object object = key.attachment();object 可以在 register 的时候传入,也可以调用 attach 方法。


最后,我们可以通过 key 的 attachment 方法,获得该对象。


selector 和 SelectionKey 我们通过 selector.select()这个一个 blocking 操作,来获取一个 ready 的 channel。


然后我们通过调用 selector.selectedKeys()来获取到 SelectionKey 对象。


在 SelectionKey 对象中,我们通过判断 ready 的 event 来处理相应的消息。


总的例子接下来,我们把之前将的串联起来,先建立一个小师妹的 ChatServer:


public class ChatServer {


private static String BYE_BYE="再见";
public static void main(String[] args) throws IOException, InterruptedException { Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 9527)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) { selector.select(); Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey selectionKey = iter.next(); if (selectionKey.isAcceptable()) { register(selector, serverSocketChannel); } if (selectionKey.isReadable()) { serverResonse(byteBuffer, selectionKey); } iter.remove(); } Thread.sleep(1000); }}
private static void serverResonse(ByteBuffer byteBuffer, SelectionKey selectionKey) throws IOException { SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); socketChannel.read(byteBuffer); byteBuffer.flip(); byte[] bytes= new byte[byteBuffer.limit()]; byteBuffer.get(bytes); log.info(new String(bytes).trim()); if(new String(bytes).trim().equals(BYE_BYE)){ log.info("说再见不如不见!"); socketChannel.write(ByteBuffer.wrap("再见".getBytes())); socketChannel.close(); }else { socketChannel.write(ByteBuffer.wrap("你是个好人".getBytes())); } byteBuffer.clear();}
private static void register(Selector selector, ServerSocketChannel serverSocketChannel) throws IOException { SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ);}
复制代码


}上面例子有两点需要注意,我们在循环遍历中,当 selectionKey.isAcceptable 时,表示服务器收到了一个新的客户端连接,这个时候我们需要调用 register 方法,再注册一个 OP_READ 事件到这个新的 SocketChannel 中,然后继续遍历。


第二,我们定义了一个 stop word,当收到这个 stop word 的时候,会直接关闭这个 client channel。


再看看客户端的代码:


public class ChatClient {


private static SocketChannel socketChannel;private static ByteBuffer byteBuffer;
public static void main(String[] args) throws IOException {
ChatClient chatClient = new ChatClient(); String response = chatClient.sendMessage("hello 小师妹!"); log.info("response is {}", response); response = chatClient.sendMessage("能不能?"); log.info("response is {}", response); chatClient.stop();
}
public void stop() throws IOException { socketChannel.close(); byteBuffer = null;}
public ChatClient() throws IOException { socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527)); byteBuffer = ByteBuffer.allocate(512);}
public String sendMessage(String msg) throws IOException { byteBuffer = ByteBuffer.wrap(msg.getBytes()); String response = null; socketChannel.write(byteBuffer); byteBuffer.clear(); socketChannel.read(byteBuffer); byteBuffer.flip(); byte[] bytes= new byte[byteBuffer.limit()]; byteBuffer.get(bytes); response =new String(bytes).trim(); byteBuffer.clear(); return response;
}
复制代码


}客户端代码没什么特别的,需要注意的是 Buffer 的读取。


最后输出结果:


server 收到: INFO com.flydean.ChatServer - hello 小师妹!client 收到: INFO com.flydean.ChatClient - response is 你是个好人 server 收到: INFO com.flydean.ChatServer - 能不能?client 收到: INFO com.flydean.ChatClient - response is 再见解释一下整个流程:志伟跟小师妹建立了一个连接,志伟向小师妹打了一个招呼,小师妹给志伟发了一张好人卡。志伟不死心,想继续纠缠,小师妹回复再见,然后自己关闭了通道。


总结本文介绍了 Selector 和 channel 在发好人卡的过程中的作用。


第十五章 文件编码和字符集 Unicode 简介小师妹一时兴起,使用了一项从来都没用过的新技能,没想却出现了一个无法解决的问题。把大象装进冰箱到底有几步?乱码的问题又是怎么解决的?快来跟 F 师兄一起看看吧。


使用 Properties 读取文件这天,小师妹心情很愉悦,吹着口哨唱着歌,标准的 45 度俯视让人好不自在。


小师妹呀,什么事情这么高兴,说出来让师兄也沾点喜庆?


小师妹:F 师兄,最新我发现了一种新型的读取文件的方法,很好用的,就跟 map 一样:


public void usePropertiesFile() throws IOException {Properties configProp = new Properties();InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties");configProp.load(in);log.info(configProp.getProperty("name"));configProp.setProperty("name", "www.flydean.com");log.info(configProp.getProperty("name"));}F 师兄你看,我使用了 Properties 来读取文件,文件里面的内容是 key=value 形式的,在做配置文件使用的时候非常恰当。我是从 Spring 项目中的 properties 配置文件中得到的灵感,才发现原来 java 还有一个专门读取属性文件的类 Properties。


小师妹现在都会抢答了,果然青出于蓝。


乱码初现小师妹你做得非常好,就这样触类旁通,很快 java 就要尽归你手了,后面的什么 scala,go,JS 等估计也统统不在话下。再过几年你就可以升任架构师,公司技术在你的带领之下一定会蒸蒸日上。


做为师兄,最大的责任就是给小师妹以鼓励和信心,给她描绘美好的未来,什么出任 CEO,赢取高富帅等全都不在话下。听说有个专业的词汇来描述这个过程叫做:画饼。


小师妹有点心虚:可是 F 师兄,我还有点小小的问题没有解决,有点中文的小小乱码….


我深有体会的点点头:马赛克是阻碍人类进步的绊脚石…哦,不是马赛克,是文件乱码,要想弄清楚这个问题,还要从那个字符集和文件编码讲起。


字符集和文件编码在很久很久以前,师兄我都还没有出生的时候,西方世界出现了一种叫做计算机的高科技产品。


初代计算机只能做些简单的算数运算,还要使用人工打孔的程序才能运行,不过随着时间的推移,计算机的体积越来越小,计算能力越来越强,打孔已经不存在了,变成了人工编写的计算机语言。


一切都在变化,唯有一件事情没有变化。这件事件就是计算机和编程语言只流传在西方。而西方日常交流使用 26 个字母加有限的标点符号就够了。


最初的计算机存储可以是非常昂贵的,我们用一个字节也就是 8bit 来存储所有能够用到的字符,除了最开始的 1bit 不用以外,总共有 128 中选择,装 26 个小写+26 个大写字母和其他的一些标点符号之类的完全够用了。


这就是最初的 ASCII 编码,也叫做美国信息交换标准代码(American Standard Code for Information Interchange)。


后面计算机传到了全球,人们才发现好像之前的 ASCII 编码不够用了,比如中文中常用的汉字就有 4 千多个,怎么办呢?


没关系,将 ASCII 编码本地化,叫做 ANSI 编码。1 个字节不够用就用 2 个字节嘛,路是人走出来的,编码也是为人来服务的。于是产生了各种如 GB2312, BIG5, JIS 等各自的编码标准。这些编码虽然与 ASCII 编码兼容,但是相互之间却并不兼容。


这严重的影响了国际化的进程,这样还怎么去实现同一个地球,同一片家园的梦想?


于是国际组织出手了,制定了 UNICODE 字符集,为所有语言的所有字符都定义了一个唯一的编码,unicode 的字符集是从 U+0000 到 U+10FFFF 这么多个编码。


小师妹:F 师兄,那么 unicode 和我平时听说的 UTF-8,UTF-16,UTF-32 有什么关系呢?


我笑着问小师妹:小师妹,把大象装进冰箱有几步?


小师妹:F 师兄,脑筋急转弯的故事,已经不适合我了,大象装进冰箱有三步,第一打开冰箱,第二把大象装进去,第三关上冰箱,完事了。


小师妹呀,作为一个有文化的中国人,要真正的承担起民族复兴,科技进步的大任,你的想法是很错误的,不能光想口号,要有实际的可操作性的方案才行,要不然我们什么时候才能够打造秦芯,唐芯和明芯呢?


师兄说的对,可是这跟 unicode 有什么关系呢?


unicode 字符集最后是要存储到文件或者内存里面的,那怎么存呢?使用固定的 1 个字节,2 个字节还是用变长的字节呢?根据编码方式的不同,可以分为 UTF-8,UTF-16,UTF-32 等多种编码方式。


其中 UTF-8 是一种变长的编码方案,它使用 1-4 个字节来存储。UTF-16 使用 2 个或者 4 个字节来存储,JDK9 之后的 String 的底层编码方式变成了两种:LATIN1 和 UTF16。


而 UTF-32 是使用 4 个字节来存储。这三种编码方式中,只有 UTF-8 是兼容 ASCII 的,这也是为什么国际上 UTF-8 编码方式比较通用的原因(毕竟计算机技术都是西方人搞出来的)。


解决 Properties 中的乱码小师妹,要解决你 Properties 中的乱码问题很简单,Reader 基本上都有一个 Charsets 的参数,通过这个参数可以传入要读取的编码方式,我们把 UTF-8 传进去就行了:


public void usePropertiesWithUTF8() throws IOException{Properties configProp = new Properties();InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties");InputStreamReader inputStreamReader= new InputStreamReader(in, StandardCharsets.UTF_8);configProp.load(inputStreamReader);log.info(configProp.getProperty("name"));configProp.setProperty("name", "www.flydean.com");log.info(configProp.getProperty("name"));}上面的代码中,我们使用 InputStreamReader 封装了 InputStream,最终解决了中文乱码的问题。


真.终极解决办法小师妹又有问题了:F 师兄,这样做是因为我们知道文件的编码方式是 UTF-8,如果不知道该怎么办呢?是选 UTF-8,UTF-16 还是 UTF-32 呢?


小师妹问的问题越来越刁钻了,还好这个问题我也有准备。


接下来介绍我们的终极解决办法,我们将各种编码的字符最后都转换成 unicode 字符集存到 properties 文件中,再读取的时候是不是就没有编码的问题了?


转换需要用到 JDK 自带的工具:


native2ascii -encoding utf-8 file/src/main/resources/www.flydean.com.properties.utf8 file/src/main/resources/www.flydean.com.properties.cn 上面的命令将 utf-8 的编码转成了 unicode。


转换前:


site=www.flydean.comname=程序那些事转换后:


site=www.flydean.comname=\u7a0b\u5e8f\u90a3\u4e9b\u4e8b 再运行下测试代码:


public void usePropertiesFileWithTransfer() throws IOException {Properties configProp = new Properties();InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties.cn");configProp.load(in);log.info(configProp.getProperty("name"));configProp.setProperty("name", "www.flydean.com");log.info(configProp.getProperty("name"));}输出正确的结果。


如果要做国际化支持,也是这样做的。


总结千辛万苦终于解决了小师妹的问题,F 师兄要休息一下。


本文的例子https://github.com/ddean2009/learn-java-io-nio


本文 PDF 下载链接 java-io-all-in-one.pdf


本文作者:flydean 程序那些事


本文链接:http://www.flydean.com/java-io-all-in-one/

发布于: 2021 年 07 月 27 日阅读数: 29
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
小师妹学IO系列文章集合-附PDF下载