第四章 文件读取那些事
简介
小师妹最新对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;