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 -n
256
看起来是你的最大文件限度太小了,只有 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("帮忙小师妹解决问题!");
}
@Override
public 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 file
if (file.createNewFile()){log.info("祝贺,文件创建胜利");
}else{log.info("不好意思,文件创建失败");
}
//Write Content
try(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 VALUE
0 12 (object header) N/A
12 1 boolean Boolean.value N/A
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space 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 敞开 watchService
- poll 获取下一个 watchKey,如果没有则返回 null
- 带工夫参数的 poll 在期待的肯定工夫内获取下一个 watchKey
- take 获取下一个 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 configurable
scheduledExecutor = Executors
.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public 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 File
implements Serializable, Comparable<File>
public interface Path
extends 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.txt
INFO com.flydean.FilePathUsage - path is : ../../www.flydean.com.txt
INFO com.flydean.FilePathUsage - absolutePath is : /Users/flydean/learn-java-io-nio/file-path/../../www.flydean.com.txt
INFO 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 - true
INFO 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 - true
hasArray 用来判断该 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 - false
INFO com.flydean.BufferUsage - true
INFO com.flydean.BufferUsage - java.nio.DirectByteBufferR[pos=0 lim=0 cap=0]
INFO com.flydean.BufferUsage - false
INFO com.flydean.BufferUsage - true
Buffer 的日常操作
小师妹: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 Buffer
rewind 和 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 Buffer
Buffer 还有一个 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:
@FunctionalInterface
public interface FilenameFilter {boolean accept(File dir, String name);
}
@FunctionalInterface
public 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 FileChannel
extends AbstractInterruptibleChannel
implements 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 的应用,敬请期待。
DatagramChannel
DatagramChannel 是用来解决 UDP 的 Channel。它自带了 Open 办法来创立实例。
来看看 DatagramChannel 的定义:
public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel
ByteChannel 示意它同时是 ReadableByteChannel 也是 WritableByteChannel,能够同时写入和读取。
MulticastChannel 代表的是一种多播协定。正好和 UDP 对应。
SocketChannel
SocketChannel 是用来解决 TCP 的 channel。它也是通过 Open 办法来创立的。
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
SocketChannel 跟 DatagramChannel 的惟一不同之处就是实现的是 NetworkChannel 借口。
NetworkChannel 提供了一些 network socket 的操作,比方绑定地址等。
ServerSocketChannel
ServerSocketChannel 也是一个 NetworkChannel,它次要用在服务器端的监听。
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel
AsynchronousSocketChannel
最初 AsynchronousSocketChannel 是一种异步的 Channel:
public abstract class AsynchronousSocketChannel
implements 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 MappedByteBuffer
extends ByteBuffer
它实际上是一个抽象类,具体的实现有两个:
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
class DirectByteBufferR extends DirectByteBuffer
implements 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 Channel
FileChannel fileChannel = file.getChannel();
//get mappedByteBuffer from fileChannel
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
// check buffer
log.info("is Loaded in physical memory: {}",buffer.isLoaded()); // 只是一个揭示而不是 guarantee
log.info("capacity {}",buffer.capacity());
//read the buffer
for (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 Channel
FileChannel fileChannel = file.getChannel();
//get mappedByteBuffer from fileChannel
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8);
// check buffer
log.info("is Loaded in physical memory: {}",buffer.isLoaded()); // 只是一个揭示而不是 guarantee
log.info("capacity {}",buffer.capacity());
//write the content
buffer.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 VALUE
0 12 (object header) N/A
12 4 byte[] String.value N/A
16 4 int String.hash N/A
20 1 byte String.coder N/A
21 1 boolean String.hashIsZero N/A
22 2 (loss due to the next object alignment)
Instance size: 24 bytes
Space 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 Closeable
Selector 是一个 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 中去。
SelectionKey
SelectionKey 示意的是咱们心愿监听到的事件。
总的来说,有 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.com
name= 程序那些事
转换后:
site=www.flydean.com
name=\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/