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("帮忙小师妹解决问题!"); } @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 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 敞开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 - 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 - 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 - 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 - 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:
@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 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 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 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 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 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.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/