前言
前面我们学习了基础的文件操作,但那些只是基于文件本身的操作,要想操作到文件里面的内容就需要用到 IO —— InputStream OutputStream 操作。那么这篇文章我将为大家分享关于 Java IO 操作相关的知识。
什么是 IO
IO 即 InputStream 和 OutputStream ,input 和 output 我们都知道是输入、输出的意思,但是后面的 stream 是什么意思呢?stream 是流的意思,既然叫“流”,那么肯定和水流是有相似的特性的。
知道了什么是“流”,那么输入和输出又是如何区分的呢?也就是说什么情况下叫输出,什么情况下又叫输出呢?
判断该操作是输入还是输出,需要有一个参照物。假设从 CPU 上向文件中传入数据的话,对于 CPU 来说,这个操作是输出,而对于文件的话,这个操作则是属于输入。
对于文件操作来说,流主要分为两类:
字节流——InputStream OutputStream
字符流——Reader Writer
虽然有两种不同的流,但是本质上还是只有一种 字节流 ,只是字符流对于字节又做出了一定的封装,字符流就是将 n 个字节当作一个字符给编码出来了。
Reader 读操作
1. 创建 Reader 类
Java 中使用 Reader 操作需要依赖于 java.io.Reader
类,但是当我们想要创建出一个 Reader 对象的话,会发现 Reader 类是一个抽象类,不能够直接创建。
所以,我们就只能创建出一个实现了 Reader 类的其他类。
Reader reader = new FileReader("d:test1/.txt");
创建 Reader 的过程就是打开文件的过程,如果文件不存在就会创建失败。
2. 理解 Reader 类中的不同 read 方法
当创建出 Reader 对象之后,就可以使用 Reader 中的方法了,而既然是要读取问价中的内容,我们就来看看如何使用 Reader 中的方法。
修饰符及返回值类型 | 方法签名 | 说明 |
int | read() | 一次读取一个字符的数据,返回 -1 代表已经完全读完了 |
int | read(char[] cbuf) | 一次读取若干个字符,直到把这个 cbuf 数组给填充满 |
int | rend(char[] cbuf, int off, int len) | 一次读取若干个字符,并且将读取到的数据从 cbuf 的off位置开始,len长度的大小填充满 |
void | close() | 关闭字符流 |
当看到上面的不同的 read 方法时候,不知道你们有没有发现:read 读取到的是字符,为什么返回类型是 int 而不是 char 呢?
在Java中,Reader类的read()方法返回int类型的原因是因为该方法需要能够表示所有可能的字符以及特殊值。
在Java中,char类型是用来表示字符的,它是一个16位的Unicode字符,可以表示65536个不同的字符。然而,read()方法需要能够返回一个特殊值来表示流的结尾(EOF,End of File)。因此,如果使用char类型作为返回类型,那么就无法表示这个特殊值。
为了解决这个问题,read()方法使用了int类型作为返回类型。int类型是一个32位的整数类型,可以表示更大的范围。在Reader类中,read()方法返回的是一个int类型的值,其中0到65535表示实际的字符,而-1表示流的结尾(EOF)。
知道了为什么 read() 方法的返回类型是 int 而不是 char 之后,还有一个问题:为什么返回的 int 范围是到 65535 的两个字节的长度,而不是三个字节的长度呢?不同的编码中字符的长度不是不相同的吗?
其实在 Java 标准库的内部,对于字符编码是做了很多操作的。
如果是只使用了 char ,此时的字符集,固定就是 Unicode,字符是两个字节
如果是使用了 String,此时就会把每个字符的 Unicode 编码转换为 utf8,字符就是三个字节了
char[] c = {'a','b','c'}; String s = new String(c);
c 中的每个字符本来是 Unicode 编码,占两个字节,但是当把字符数组转换为字符串的时候,每个字符的编码就从 Unicode 转换为 utf8 编码了。
char cc = s.charAt(1);
当使用这个操作的时候,cc 中得到的字符又会从 utf8 编码转化为 Unicode 编码。
3. 使用 Reader 类当中的不同 read 方法
read() 一次读取一个字符
这个方法的返回值,就是读取到的字符转换为 int 类型
public class Demo1 { public static void main(String[] args) throws IOException { Reader reader = new FileReader("d:/test.txt"); while (true) { //这个read方法中没有传入参数,一次只读取一个字符 int ret = reader.read(); if (ret == -1) break; char ch = (char)ret; System.out.println(ch); } reader.close(); } }
read(char[] cbuf) 一次读取多个字符
这个方法的返回值是一次读取到了多少个字符
public class Demo2 { public static void main(String[] args) throws IOException { try (Reader reader = new FileReader("d:/test.txt")) { while (true) { char[] cbuf = new char[3]; //这个read方法的返回值是读取到的字符的个数 int ret = reader.read(cbuf); if (ret == -1) break; for (int i = 0; i < ret; i++) { System.out.println(cbuf[i]); } } } } }
第三个 read 方法跟这个 read 方法类似,只是第三个方法有偏移量这个参数,通常用在读取多个文件的场景下,我们这里就不过多介绍了。
3. 关闭文件操作
当使用完文件之后,我们需要 reader.close()及时关闭文件,防止造成资源泄露。但是这样的话,如果在执行这段代码之前发生了异常,造成程序终止的话,那么这个文件就不会被关闭,所以就需要使用一个方法,使得整个关闭的操作无论如何都要执行到。那么用什么办法可以使得整个关闭操作无论如何都能执行到呢?
try { //... } finally { reader.close(); }
使用 try - finally 可以保证 finally 当中的代码无论如何都会执行到。
但是还有一种方法就可以实现这个功能,又能做到优雅,着中方法是什么办法呢?
try (Reader reader = new FileReader("d:/test.txt")) { //.... }
这种语句叫做 try-with-resources
语句,这个语句的特性就是,当括号中的对象实现了 closerable
的话,当 {} 内的代码执行完之后,会自动调用该对象当中的 close()
方法。
public class Demo1 { public static void main(String[] args) throws IOException { try (Reader reader = new FileReader("d:/test.txt")) { while (true) { //这个read方法中没有传入参数,一次只读取一个字符 int ret = reader.read(); if (ret == -1) break; char ch = (char)ret; System.out.println(ch); } } } }
为什么最后不关闭文件,会造成资源的泄露呢?
前面我们学习进程和线程的时候都知道,每当创建一个进程的时候,就会有一个对应的 PCB ,而 PCB 中含有多种属性,像 pid、内存指针、文件描述符表 等,当在一个进程中打开一个文件的时候,就需要在这个文件描述符表中分配一个元素,而这写元素是存在于一个数组中的,数组的长度是有限制的,如果你打开一个文件,并且一直不关闭这个文件的话,这个数组中的元素就会越来越多,直到出现问题。
所以,一定要记得关闭文件!关闭文件!关闭文件!
Writer 写操作
Writer 是将数据以字符的形式写入文件中,在 Java 中,使用 Writer 方法需要借助 java.io.Writer
类,并且这个类也是属于抽象类,不能直接创建出对象,只能创建出它的子类。
1. 创建出 Writer 类
public class Demo3 { public static void main(String[] args) { try (Writer writer = new FileWriter("d:/test1.txt")) { } catch (IOException e) { throw new RuntimeException(e); } ; } }
在创建 Writer 对象的时候,如果该文件不存在,并不会报错,而是会自动创建出该文件。
2. 理解 Writer 类中的不同 write 方法
Writer 类中的 write 方法是比较容易理解的,只是参数不同,我们可以将字符对应的 ASCII 码转换的整数作为参数传入,也可以传入字符串、字符数组,并且当传入字符串和字符数组的时候还可以指定偏移量和长度。
3. 使用 Writer 类当中的 write 方法
public class Demo3 { public static void main(String[] args) { try (Writer writer = new FileWriter("d:/test1.txt")) { writer.write("要想成为Java高级工程师,就需要不断地敲代码"); } catch (IOException e) { throw new RuntimeException(e); } ; } }
这里在执行代码之前,该文件当中是有内容的,然后执行代码之后,我们看看结果。
可以发现,这个文件之前的内容被覆盖掉了,也就是说:写操作会默认覆盖掉之前写入的内容。
那么,我如果不想覆盖写入而是追加写入的时候该怎么办呢?
要想实现写操作的时候是追加写入而不是覆盖写入的时候,我们可以在创建 Writer 对象的时候多传一个参数。
这里传入的参数 true 就表示 append 为 true,意思就是追加写入的意思。
public class Demo3 { public static void main(String[] args) { try (Writer writer = new FileWriter("d:/test1.txt",true)) { writer.write("要想成为Java高级工程师,就需要不断地敲代码"); } catch (IOException e) { throw new RuntimeException(e); } ; } }
OutputStream 字节流输出
当知道了如何使用 Writer 以字符流的行书写入之后,OutputStream 以字节流的形式写入的方式也很简单。
1. 创建出 Outputtream 对象
public class Demo4 { public static void main(String[] args) { try (OutputStream outputStream = new FileOutputStream("d:/test2.txtx")) { } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } ; } }
理解 OutputStream 的 write 方法
当参数为 int 类型的时候,int 对应的是 ASCII 码中的对应字符,另外两个出纳惨的方式跟上面是一样的,这里我就不过多解释了。
使用 OutputStream 的 write 方法
public class Test { public static void main(String[] args) { try (OutputStream outputStream = new FileOutputStream("d:test.txt")) { outputStream.write('a'); outputStream.write('b'); outputStream.write('c'); outputStream.write('d'); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }
这里当我们以字节的形式写入数据的时候,可能会发生问题。
为什么运行程序之后,test.txt
文件中没有数据呢?
其实在写入数据的过程中,并不是直接将内存中的数据写入文件中的,而是在这个过程中还会经过一个特殊的内存空间——缓冲区。当数据传入缓冲区的时候,缓冲区并不会立即就将数据传输给文件中,而是当缓冲区中的数据到达一定大小之后,将这些数据一起传输给文件。那么为什么会有这个步骤呢?
数据从内存传输到缓冲区速度是很快的,但是从缓冲区传输到文件中的速度是很慢的,如果缓冲区中接收到了数据之后就将这些数据传输到文件中的话,就需要传输很多次,就类似于:我是一个送水工,负责给一个小区的居民送水,这时我收到了一个请求,我就立即带上一桶水过去送水的,但是呢我在送水的过程中又收到了一个送水请求,第二个请求我也只能等第一桶水送到了之后回来重新装一桶水才能继续给第二个人送水。这样的效率很慢,而且很废人力,所以我就可以现在公司等一会,就算有了送水请求我也不是立即就去,而是等请求到达了一定量之后,统一将这些水装入送货车上一起送,这样既节省了时间,也节省了人力,这也就是缓冲区的的作用。
既然我们知道了在写入数据的时候会经过缓冲区,那么为什么写入数据的时候,数据最终没有被写入呢?这是因为缓冲区中的数据太少了,还没到达将数据传入文件的大小,所以缓冲区就不会将接收到的数据传入文件,而这时由于程序执行结束了,当程序执行结束之后,缓冲区中的数据就会被释放掉了,最终导致数据没有被写入到文件中。
所以为了解决这个问题,当我们写完数据了之后需要刷新一下缓冲区。outputStream.flush
public class Test { public static void main(String[] args) { try (OutputStream outputStream = new FileOutputStream("d:test.txt")) { outputStream.write('a'); outputStream.write('b'); outputStream.write('c'); outputStream.write('d'); outputStream.flush(); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }
向文件中写入字符数据,说的是字符数据,但是要求的参数类型是字节,所以需要对字符做出转换。
public class Demo4 { public static void main(String[] args) { try (OutputStream outputStream = new FileOutputStream("d:/test2.txt")) { String s = "我爱中国"; outputStream.write(s.getBytes()); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } ; } }
利用 PrintWriter 来进行输出
上述,我们其实已经完成输出工作,但总是有所不方便,我们接来下将 OutputStream 处理下,使用
PrintWriter 类来完成输出,因为PrintWriter 类中提供了我们熟悉的 print/println/printf 方法
public class Test2 { public static void main(String[] args) { try (OutputStream outputStream = new FileOutputStream("d:/test.txt")) { PrintWriter printWriter = new PrintWriter(outputStream); printWriter.print("你好世界"); printWriter.println("我爱Java"); printWriter.println("我要成为Java高级工程师"); printWriter.flush(); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }
InputStream 字节流输入
InputStream 是以字节流的形式输入,在 Java 中,使用 InputStream 需要借助 java.io.InputStream
中的 InputStream 类。
1. 创建出 InputStream 类
public class Test { public static void main(String[] args) { try (InputStream inputStream = new FileInputStream("test.txt")) { } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }
2. 了解 InputStream 类中的 read 方法
InputStream 中的 read 方法和 Reader 类当中的 read 方法是类似的,只是 InoutStream 类当中的 read 方法传入的参数是 byte 类型的。
3. 使用 InputStream 类当中的 read 方法
public class Test { public static void main(String[] args) { try (InputStream inputStream = new FileInputStream("d:/test.txt")) { while (true) { int ret = inputStream.read(); if (ret == -1) break; System.out.println(ret); } } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }
这样的输出结果就是文件中所有字符的二进制形式。
public class Test2 { public static void main(String[] args) { try (InputStream inputStream = new FileInputStream("d:/test.txt")) { while (true) { byte[] b = new byte[16]; int n = inputStream.read(b); if (n == -1) break; for (int i = 0; i < n; i++) { System.out.println(b[i]); } } } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }
利用 Scanner 进行字符的读取
上述例子中,我们看到了对字符类型直接使用 InputStream 进行读取是非常麻烦且困难的,所以,我
们使用一种我们之前比较熟悉的类来完成该工作,就是 Scanner 类。
public class Test3 { public static void main(String[] args) { try (InputStream inputStream = new FileInputStream("d:/test.txt")) { Scanner scanner = new Scanner(inputStream); String s = scanner.next(); System.out.println(s); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }
我们平时使用 Scanner 的话,传入的参数往往是 System.in
这是标准输入也就是键盘输入,而如果传入文件的话,就是从文件中输入。
运用文件操作和IO实现一个删除指定文件的功能
学习了如何使用文件操作和IO操作之后,我们就运用这些知识来实现一个删除指定文件的功能。这个功能的要求是什么呢?就是需要提示用户输入删除的是哪个目录下的文件,然后提示用户输入需要删除文件的关键词。
根据要求,我们知道这个题目其实就是遍历用户输入的目录下面的所有文件,既然文件底层的数据结构是数,那么就需要使用递归的思想,当递归到文件夹的时候,就继续递归,当递归到文件的时候,就判断这个文件是否含有用户输入的指定关键词。
public class Demo { public static void main(String[] args) { System.out.println("请输入要扫描的文件路径"); Scanner scanner = new Scanner(System.in); String path = scanner.next(); File rootPath = new File(path); //判断用户输入的扫描文件是否合法 if (!rootPath.isDirectory()) { System.out.println("您输入的扫描文件的路径有误"); return; } System.out.println("请输入要删除的文件的关键词"); String word = scanner.next(); scanDir(rootPath,word); } private static void scanDir(File rootPath, String word) { File[] files = rootPath.listFiles(); if(files == null) return; for (File f : files) { //添加一个日志,知道遍历到哪个文件了 System.out.println("当前扫描文件" + f.getAbsolutePath()); if (f.isFile()) { chechDelete(f,word); }else { scanDir(f,word); } } } private static void chechDelete(File f, String word) { if (!f.getName().contains(word)) return; System.out.println("当前文件为" + f.getAbsolutePath() + ", 请确认是否要删除(Y/N)"); Scanner scanner = new Scanner(System.in); String choice = scanner.next(); if (choice.equals("Y")) { f.delete(); System.out.println("删除完毕"); }else { System.out.println("取消删除"); } } }
复制指定文件
public class Demo1 { public static void main(String[] args) { //复制文件到指定路径 Scanner scanner = new Scanner(System.in); System.out.println("请输入你要复制的文件:"); String srcPath = scanner.next(); File srcFile = new File(srcPath); if (!srcFile.exists()) { System.out.println("您输入的文件不存在,请确认文件路径的正确性"); return; } if (!srcFile.isFile()) { System.out.println("您输入的文件不是普通文件,请确认文件路径的正确性"); return; } System.out.println("请输入要复制到的目标路径"); String desPath = scanner.next(); File desFile = new File(desPath); if (desFile.isDirectory()) { System.out.println("您输入的路径是一个目录,不是文件,请确认文件路径是否正确"); return; } if (desFile.isFile()) { if (desFile.exists()) { System.out.println("您要复制到的文件已存在,是否需要覆盖:Y/N"); String choice = scanner.next(); if (!choice.equals("Y")) return; } } try (InputStream inputStream = new FileInputStream(srcFile)) { try (OutputStream outputStream = new FileOutputStream(desFile)) { byte[] b = new byte[1024]; while (true) { int n = inputStream.read(b); if (n == -1) break; outputStream.write(b,0,n); } } } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }
在指定目录中查找文件名或文件内容含有关键词的文件
public class Demo2 { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入你要扫描的目录"); String rootDirPath = scanner.next(); File rootDir = new File(rootDirPath); if(!rootDir.isDirectory()) { System.out.println("您输入的路径不是正确的目录路径,请检查你输入的路径"); return; } System.out.println("请输入你要查询的文件名或者文件内容中包含的关键词"); String word = scanner.next(); List<File> list = new ArrayList<>(); scanDir(rootDir,word,list); for (File s : list) { System.out.println(s.getAbsoluteFile()); } } private static void scanDir(File rootDir, String word, List<File> list) { File[] files = rootDir.listFiles(); if (files == null) return; for (File f : files) { if (f.isDirectory()) { scanDir(f,word,list); }else { if (isContainsContent(f,word)) { list.add(f); } } } } private static boolean isContainsContent(File f, String word) { if (f.getName().contains(word)) return true; StringBuilder stringBuilder = new StringBuilder(); try (InputStream inputStream = new FileInputStream(f.getAbsoluteFile())) { try (Scanner scanner = new Scanner(f.getAbsoluteFile())) { while (scanner.hasNextLine()) { stringBuilder.append(scanner.nextLine()); if (stringBuilder.indexOf(word) != -1) return true; } return false; } } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } }