二、InputStream
我们在当前的工作目录下,创建两个 .txt 文件,里面的内容如下:
现在我们通过输入流来测试一下读取文件中的内容。
测试一
程序清单8:
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class Test8 { public static void main(String[] args) throws IOException { //创建实例的过程:先打开文件,再进行读写 InputStream inputStream = new FileInputStream("./test.txt"); while (true) { //read 无参版本一次只能读取一个字节,返回值即当前读到的字节 //如果读到文件的末尾,就会返回 -1 int a = inputStream.read(); if (a == -1) { break; } System.out.printf("%c", a); } //这里记得关闭流 inputStream.close(); } }
输出结果:
注意几个点:
① InputStream 是抽象类,它本身不能实例化对象,我们只能使用它的实现类来完成 new
② 程序清单8 中的 read 方法是无参的方法,一次只能读一个字节,另外,当读到文件末尾的时候,就会返回 -1,即 EOF ( end of file ),文件结束符。
③ 如果我们读取的是英文,直接转换成字符的形式输出,否则就会输出数字,即 ASCII 码。
④ 在程序的最后,需要使用 close 方法来关闭流,以防资源泄露。一个进程能打开的文件数量是有上限的,其能打开的文件描述符表也是有上限的,如果不使用 close 及时关闭,那么当文件描述表中的资源占满的时候,就会使程序崩溃。
举个例子:这就和我们平时使用自来水一样,当你用完了水龙头,需要关闭,否则,你就会浪费水资源!如果一个小区都这样浪费水的话,那么很快就会发生水资源不足,因为水资源是有上限的。
优化代码:
try with resources 语法
在我们解决了所有的问题后,将上图的左边代码转换成了右边代码,但我们可以发现,右边的代码又较为繁杂,所以我们可以使用 【 try with resources 】这个语法,即在 try( ) 中添加类,那么哪些类可以放入这个 try 的括号中呢?
我们查看输入流和输出流都实现了 Closeable 接口,在这个接口的源代码中,我们看到它只有一个方法 close,也就是说:实现了这个 Closeable 接口的类,当我们在 try( ) 中添入的类,这个类就会自动使用 close 方法来关闭资源。
try( ) 这个方法更加简单,而且能够自动使用 close 方法关闭流。也需要我们重点掌握,如下程序清单9。
程序清单9:
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class Test9 { public static void main(String[] args) throws IOException { try ( InputStream inputStream = new FileInputStream("./test.txt") ) { while (true) { int a = inputStream.read(); if (a == -1) { break; } System.out.printf("%c", a); } } catch (IOException e) { e.printStackTrace(); } } }
测试二
程序清单10:
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Test10 { public static void main(String[] args) { try (InputStream inputStream = new FileInputStream("./test.txt") ){ byte[] buffer = new byte[1024]; while (true) { int len = inputStream.read(buffer); if (len == -1) { break; } for (int i = 0; i < len; i++) { System.out.printf("%c", buffer[i]); } } } catch (IOException e) { e.printStackTrace(); } } }
输出结果:
程序清单10 中的 read 方法会尝试把参数的这个 buffer 给填满,buffer 数组共 1024 个字节。
假设该文件的长度是2049,下面是读取文件的总过程:
第一次循环,读取出1024个字节,放到 buffer 数组中,read 方法返回1024,
第二次循环,读取出1024个字节,放到 buffer 数组中,read 方法返回1024,
第三次循环,读取出1个字节,放到 buffer 数组中,read 方法返回1,
第四次循环,此时已经读到文件末尾了 ( EOF ),read 方法返回-1.
注意几个点:
① 程序清单10 中的 read 方法,一次可以读取多个字节。
我们需要明确一个点,读取数据的速度: CPU 上的寄存器 >> 内存 >> 磁盘 ( A >> B,表示 A 的速度远快于 B )
也就是说,相比于一次只读一个字节,这样一次读取多个字节的效果更好,因为一次多读点数据,那么读的次数就会减少,那么磁盘的 IO 次数就会减少,效率也就更高!
举个例子:小明的宿舍住三楼,他准备去一楼的水房接开水,他准备接 1 L 的水,但他打算用水杯接水,所以他来来回回地上下楼跑了好多次。下一次的时候,他心想:用水杯接水太麻烦了,于是他就拿着两个水瓶接水了,这样效率就高很多了。而这就对应着上面的读操作,一次多接点水,那么跑路就少很多。
② read 方法的返回值是一次读取的字节长度,如果读的不为空文件,那么至少一次读取1个字节,和之前一样,当读到文件末尾,read 方法就返回 -1.
测试三
我们来测试一下读取内容是中文的文件。
程序清单11:
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; public class Test11 { public static void main(String[] args) { try (InputStream inputStream = new FileInputStream("./test3.txt") ) { try (Scanner scanner = new Scanner(inputStream, "UTF-8")) { while (scanner.hasNext()) { String s = scanner.nextLine(); System.out.println(s); } } } catch (IOException e) { e.printStackTrace(); } } }
输出结果:
注意几个点:
① 在程序去清单11 中,我们这里使用 Scanner 对象,并不是我们之前的 System.in 的标准输入。在这里,我们传入的参数分别是:【 流,字符集】。同样地,这里的 Scanner 对象涉及到流了,那么就得使用 close 方法,当然,使用【 try with resources 】这个语法也行得通!
② 使用 Scanner 这个语法也能读取到中英文混合的文件,我们可以使用 scanner.nextLine 方法来实现这个读取操作。
三、OutputStream
对 flush 方法进行说明:我们知道,计算机读写内存数据的速度是比读写磁盘数据的速度是要快很多的。所以,大多数的 OutputStream 为了减少访问磁盘的次数,在写数据的时候都会将数据暂时先写入内存的缓冲区中,直到该区域满了或者其他指定条件时才真正将数据写入磁盘中。但这可能会造成一个结果,就是我们写的数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置,调用 flush(刷新)操作,将数据刷到设备中,这就是 " 临门一脚 "。
举个例子:当一家人嗑瓜子的时候,不需要嗑一个瓜子,就把瓜子皮往垃圾桶里放,垃圾桶就一个,如果所有人将瓜子皮一个一个往里放,效率太低。我们可以先把瓜子皮放在手中,等吃完了所以瓜子,再一下子放入垃圾桶中。那么最后将手中所以的瓜子皮全放入到垃圾桶中,这个操作就相当于 flush 操作,即刷新缓冲区。
我们在当前的工作目录下,创建一个 .txt 文件,里面的内容如下:
现在我们通过输出流来测试一下读取文件中的内容。
测试一
程序清单12:
import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; public class Test12 { public static void main(String[] args) { //一旦按照 OutputStream 的方式打开文件,就会把文件中的原来内容给清空掉 try (OutputStream outputStream = new FileOutputStream("./test.txt") ) { //写入一个字符 outputStream.write('a'); outputStream.write('b'); outputStream.write('c'); } catch (IOException e) { e.printStackTrace(); } } }
文件写入结果:
程序清单13:
import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; public class Test13 { public static void main(String[] args) { try (OutputStream outputStream = new FileOutputStream("./test.txt") ) { //按照字节来写入 byte[] bytes = new byte[] { (byte)'O',(byte)'P',(byte)'Q' }; outputStream.write(bytes); } catch (IOException e) { e.printStackTrace(); } } }
文件写入结果:
程序清单14:
import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; public class Test14 { public static void main(String[] args) { try (OutputStream outputStream = new FileOutputStream("./test.txt") ) { //按照字符串来写入 String s = "welcome to the world !"; outputStream.write(s.getBytes()); } catch (IOException e) { e.printStackTrace(); } } }
文件写入结果:
测试二
程序清单15:
import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; public class Test15 { public static void main(String[] args) { //使用 try (OutputStream outputStream = new FileOutputStream("./test.txt") ) { try (PrintWriter printWriter = new PrintWriter(outputStream)) { printWriter.print("Hello, Jack "); printWriter.println("欢迎来到这个世界!"); } } catch (IOException e) { e.printStackTrace(); } } }
文件写入结果:
总结
注意几个点:
① OutputStream 和 InputStream 这两个类在用法上差不多,它也需要通过 close 关闭流。
② 正常情况下,按照 OutputStream 的方式打开文件,就会把文件中的原来内容给清空掉,原来的文件内容将被直接替换成重新写入的内容。但我们也可以自己设置,将重新写的内容续在文件的末尾。
只需要将 new FileOutputStream 的第二个参数设置为 true 即可。
try ( OutputStream outputStream = new FileOutputStream(filePath, true) )
③ OutputStream 类支持以字符写入、以字节写入、也可以按照以中英文混合写入。
四、案例
案例1 检索文件名为 key 的普通文件
指定一个待检测的目录,并扫描这个目录,查找整个目录所有普通文件的文件名带有关键字 key,符合关键字 key 的普通文件,展示出来,最后询问用户是否删除。
假设我要删除 D 盘下文件目录下的 test5.txt,很显然,我只要让它递归去跑即可,这就和树的查找很类似了!
程序清单16:
import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class Test16 { public static void main(String[] args) { //1. 让用户输入一个待扫描的目录、待查询的关键字 Scanner scanner = new Scanner(System.in); System.out.println("请输入你所要查找的目录对应的绝对路径:"); String root = scanner.nextLine(); File rootDir = new File(root); if(!rootDir.isDirectory()) { System.out.println("你所输入的不是目录,即将退出程序!"); return; } System.out.println("请输入你所要查找的关键字:"); String key = scanner.nextLine(); //2. 递归整个目录 // list 中表示递归遍历的结果,里面存放着所有带 key 关键字的文件路径 List<File> list = new ArrayList<>(); finding(rootDir,key,list); //3. 遍历 list,询问用户是否要删除该文件,再根据用户的输入来决定是否要删除 for (File f : list) { System.out.println("是否要删除该文件:" + f.getName()); System.out.println("删除请输入 1 / 保留请输入 0"); int r = scanner.nextInt(); if (r == 1) { f.delete(); break; } } } /** * 用 finding 方法进行递归查找 */ public static void finding(File rootDir, String key, List<File> list) { //从主目录出发,往下搜寻对应的文件夹和普通文件 File[] files = rootDir.listFiles(); if (files == null || files.length == 0) { //当前目录是一个空目录,就直接返回 return; } for (File f : files ) { if(f.isDirectory()) { //如果当前的文件是一个目录,就继续递归查找 finding(f, key, list); } else { if( f.getName().contains(key) ) { list.add(f.getAbsoluteFile()); } } } } }
输出结果:
案例2 普通文件的全文检索
扫描指定目录,并找到内容中包含指定内容关键字的所有普通文件。
假设我们在文件目录下的测试目录下查找内容包含 【world】关键字的普通文件,如果存在,就打印出普通文件的路径。
程序清单17:
import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class Test17 { public static void main(String[] args) { //1. 输入起始目录和内容关键字 Scanner scanner = new Scanner(System.in); System.out.println("请输入起始目录:"); String srcPath = scanner.nextLine(); File rootDir = new File(srcPath); if (!rootDir.isDirectory()) { System.out.println("你所指定的不是目录,程序即将退出!"); return; } System.out.println("请输入查询的内容关键字:"); String keyword = scanner.nextLine(); //2. 利用递归来查找内容 List<File> list = new ArrayList<>(); finding(rootDir,keyword, list); //3. 打印出所有符合要求的文件名 for (File f : list) { System.out.println(f.getAbsoluteFile()); } } /** * 利用递归来找到包含指定 keyword 的普通文件,并所有符合要求的放入顺序表中 */ public static void finding(File rootDir, String keyword, List<File> list) { //从主目录出发,往下搜寻对应的文件夹和普通文件 File[] files = rootDir.listFiles(); if (files == null || files.length == 0) { return; } for (File f : files) { if (f.isDirectory()) { finding(f, keyword, list); }else{ if ( isContains(f, keyword) ){ list.add(f); } } } } /** * 判断是否满足普通文件中包含关键字 keyword,利用主串与子串的关系来解决 */ private static boolean isContains(File f, String keyword) { StringBuilder stringBuilder = new StringBuilder(); try( InputStream inputStream = new FileInputStream(f.getAbsoluteFile()) ) { Scanner scanner = new Scanner(inputStream, "utf-8"); while (scanner.hasNext()) { //将普通文件中的所有内容全部拼接至 stringBuilder 中 String str = scanner.nextLine(); stringBuilder.append(str + "\n" ); } } catch (IOException e) { e.printStackTrace(); } //看看主串 stringBuilder 中是否有子串 keyword,如果有,必定不是-1 return stringBuilder.indexOf(keyword) != -1; } }
测试结果:
在程序清单17 中的代码,在扫描目录和普通文件数量较多时,效率较低,因为它涉及到了密集的磁盘 IO,每一次查询都要重新扫一遍磁盘,扫描的工作都是在查询中进行,所以较慢。举个例子:小明口渴了,准备从家里出发去小卖部买水,于是他就得跑过去再跑回来,当回家的时候,他的妈妈说,烧菜的时候缺盐,于是小明又得重新从家里出发,去便利店买了袋盐。
有一个软件叫做 《Everything》,这个软件的查询速率十分高效,当你按文件名搜索的时候,2秒钟不到,磁盘中的所有数据都能够按照关键字检索出来,相比于 windows 的直接搜索,Everything 这个软件极快。而 Everything 这个软件就是在查找数据之前,其通过了一些数据结构对磁盘数据做了预处理,做足了一些准备工作,所以快是有原因的。
举个例子:刚刚小明需要什么东西才出门买,那么当小明从家里出发之前,如果事先列好一份清单,再制定出行程路线,那么效率自然就上去了。
案例3 复制普通文件
编写代码复制一个文件,要求这个文件是一个普通文件,不是目录。启动程序之后,让用户指定一个待复制文件的绝对路径,再指定一个粘贴文件的终止路径。再通过这两个路径,来完成复制文件的逻辑。
假设我要让文件目录下的 BBB目录中 的 1.txt 和 1.png 分别复制成 2.txt 和 2.png
程序清单18:
import java.io.*; import java.util.Scanner; public class Test18 { public static void main(String[] args) { //1. 让用户输入一个起始路径和一个目标路径 Scanner scanner = new Scanner(System.in); System.out.println("请输入需要复制文件的起始路径:"); String srcPath = scanner.nextLine(); File srcFile = new File(srcPath); //2. 判断几种特殊情况 //2.1 所复制的文件不存在 或 不是一个文件格式 if (!srcFile.exists() || !srcFile.isFile()) { System.out.println("你所要复制的文件不存在 或 它不是一个文件格式,请检查格式!"); return; } System.out.println("请输入需要粘贴的终止路径:"); String destPath = scanner.nextLine(); File destFile = new File(destPath); //2.2 所复制的文件在终止路径下重名 if (destFile.exists()) { System.out.println("你所要复制的文件在终止路径下已经存在,即将退出程序!"); return; } //2.3 所复制的文件的父目录不存在 if (!destFile.getParentFile().exists() ) { System.out.println("你所要复制的文件的终止路径不存在,即将退出程序!"); return; } //3. 先读待复制的文件,再将读到的内容写入到终止路径中 try ( InputStream inputStream = new FileInputStream(srcFile); OutputStream outputStream = new FileOutputStream(destFile) ) { while (true) { byte[] buffer = new byte[1024]; int len = inputStream.read(buffer); if (len == -1) { break; } //这里应该注意 len 的范围,当 len > 1024 还好说 //但假设 len = 500 < 1024 的时候,我们就必须控制范围, //最好不要超过当前 len 的值,防止写入一些无效的数据 //outputStream.write(buffer); //不建议使用带有一个参数的 write 方法 outputStream.write(buffer,0,len); } //刷新缓冲区 //这里如果不加 flush,try() 会自动触发 close 方法,也就能自动刷新缓冲区 outputStream.flush(); } catch (IOException e) { e.printStackTrace(); } } }
测试结果: