• 文件内容的操作


    目录

    1. 什么是流

     

    2. 读文件

    2.1 字节流-InputStream / FileInputStream

    2.2 字符流-Reader / FileReader

    2.3 通过 Scanner 进行字符读取

    2.4 关闭资源的优化

    3.写文件

    3.1 字节流-OutputStream / FileOutputStream

    3.2 字符流-Writer / FileWriter

    4.小程序示例


    1. 什么是流


    2. 读文件

    2.1 字节流-InputStream / FileInputStream

    【方法】

    修饰符及
    返回值类
    方法说明
    int
    read()读取一个字节的数据,返回 -1 代表已经完全读完了
    int
    read(byte[] b)
    最多读取 b.length 字节的数据到 b 中,返回实际读到的数
    量;返回 -1 代表以及读完了
    int
    read(byte[] b, int off, int len)
    读数据到 b 数组中,从  b[ off ]   开始放,返
    回实际读到的数量; -1 代表以及读完了
    void
    close()

    关闭字节流

    InputStream 是一个抽象类,不能直接实例化,它的实现类有很多,我们现在只关心读,所以可以通过 FileInputStream 来实现。

    🍃打开文件

    InputStream inputStream = new FileInputStream("test2.txt");

    对于这一行代码,打开文件成功后,就得到了 InputStream 对象,后续针对文件的操作,就都是通过这个对象展开的。

    再者,我们操作硬盘不方便直接操作,在内存里构造一个和它关联的对象,操作这个对象就相当于操作硬盘数据。

    上述操作对象相当于操作硬盘数据,就类似于遥控器,我们开空调的时候,不可能每次借助梯子爬上去,用手摁下空调开关,而是通过空调遥控器去操作空调。

    🍃关闭文件

    1. inputStream.close();
    2. // 此处会抛出一个 IOException,对于打开文件抛出的文件找不到异常,IOException是其父类

    为什么要关闭文件??

    关闭文件主要是为了释放文件描述符表资源。

    文件描述符表,这个表就相当于是个数组,这个数组的下标就称为 "文件描述符",每次打开一个文件,都会在文件描述符表中占据一个位置;每次关闭文件,都会释放一个位置。并且文件描述符表,是存在上线的,如果一个进程,一直在打开文件,没有释放,此时就会导致我们的进程在进行后续打开文件操作的时候,就会打开文件失败!!

    🍃读取文件

    🍔read() 代码示例

    1. public static void main(String[] args) throws IOException {
    2. // 1.打开文件
    3. InputStream inputStream = new FileInputStream("test2.txt");
    4. // 2.读取文件
    5. while(true) {
    6. int b = inputStream.read();
    7. if(b == -1) {
    8. // 文件读完了
    9. break;
    10. }
    11. // 因为 ASCII 码值以字节为单位存储,所以这里输出的是每个单词对应的 ASCII 码值
    12. System.out.println(b); // hello 对应的 ASCII 码值
    13. }
    14. // 3.关闭文件
    15. inputStream.close();
    16. }

    🍔read(byte[] b)代码示例

    1. public static void main(String[] args) throws IOException {
    2. // 1.打开文件
    3. InputStream inputStream = new FileInputStream("test2.txt");
    4. // 2.读取文件
    5. byte[] buf = new byte[1024];
    6. // 返回的长度(字节)
    7. int len = inputStream.read(buf);
    8. System.out.println(len);
    9. for(int i = 0; i < len; i++) {
    10. System.out.println(buf[i]);
    11. }
    12. // 3.关闭文件
    13. inputStream.close();
    14. }

    注意: 如果是读取中文,UTF8 的编码格式,一个汉字占3个字节。

    如果要想读取中文在控制台,字节流只能通过指定编码格式,就比较麻烦。例如以下代码:

    1. public static void main(String[] args) throws IOException {
    2. // 1.打开文件
    3. InputStream inputStream = new FileInputStream("test2.txt");
    4. // 2.读取文件
    5. byte[] buf = new byte[1024];
    6. int len = inputStream.read(buf);
    7. String s = new String(buf, 0, len, "UTF8");
    8. System.out.println(s);
    9. // 3.关闭文件
    10. inputStream.close();
    11. }

    为了解决上述问题,我们就可以通过字符流来进行读取。

    2.2 字符流-Reader / FileReader

    这里的方法和上面的 InputStream 相似就不过多介绍了。

    🍔读取中文代码示例

    1. public static void main(String[] args) throws IOException {
    2. // 字符流
    3. Reader reader = new FileReader("test2.txt");
    4. char[] buffer = new char[1024];
    5. int len = reader.read(buffer);
    6. for(int i = 0; i < len; i++) {
    7. System.out.println(buffer[i]);
    8. }
    9. reader.close();
    10. }

    虽然字符流读取中文比纯字节流好使一些,但还是不够方便。对于文本文件,还有更简单的写法。

    2.3 通过 Scanner 进行字符读取

    🍁Scanner 构造方法

    构造方法
    说明
    Scanner(InputStream is, String charset)

    使用 charset 字符集进行 is 的扫描读取

    (也可以不指定编码格式,使用默认编码格式)

    代码示例

    1. public static void main(String[] args) throws IOException {
    2. InputStream inputStream = new FileInputStream("test2.txt");
    3. Scanner scanner = new Scanner(inputStream);
    4. // 想读什么类型就用 scanner 去调用什么
    5. String s = scanner.next();
    6. System.out.println(s);
    7. inputStream.close();
    8. }

    2.4 关闭资源的优化

    我们在前面字节流读取中文的代码中,发现如果在 read() 读文件的过程中出现异常,就可能导致 close() 执行不到,按照我们以前的思路,使用 try...catch...finally,将关闭资源放在 finally 中。

    1. public static void main(String[] args) {
    2. InputStream inputStream = null;
    3. try {
    4. // 打开文件(字节流)
    5. inputStream = new FileInputStream("test2.txt");
    6. byte[] buf = new byte[1024];
    7. int len = inputStream.read(buf);
    8. // 读取中文
    9. String s = new String(buf, 0, len, "UTF8");
    10. System.out.println(s);
    11. } catch (IOException e) {
    12. e.printStackTrace();
    13. } finally {
    14. // 关闭资源
    15. try {
    16. inputStream.close();
    17. } catch (IOException e) {
    18. e.printStackTrace();
    19. }
    20. }
    21. }

    如果这样做的话,我们的代码显得又臭又长,更推荐的做法是以下做法:

    1. public static void main(String[] args) {
    2. try (InputStream inputStream = new FileInputStream("test2.txt")) {
    3. byte[] buf = new byte[1024];
    4. int len = inputStream.read(buf);
    5. // .........
    6. } catch (IOException e) {
    7. e.printStackTrace();
    8. }
    9. }

    这种语法机制叫做 try with resources,这个操作就会在 try 执行结束后,自动调用 inputStream close 方法(实现 Closeable 接口的类才能这样做)。


    3.写文件

    3.1 字节流-OutputStream / FileOutputStream

    【方法】

    修饰 符及 返回 值类
    方法说明
    void
    write(int b) 写入要给字节的数据
    void
    write(byte[] b)
    b 这个字符数组中的数据全部写入 os
    int
    write(byte[] b, int off,
    int len)
    b 这个字符数组中从 off 开始的数据写入 os 中,一共写 len
    voidclose()关闭字节流
    void
    flush()
    I/O 的速度是很慢的,所以,大多的 OutputStream 为了减少设备操作的次数,在写数据的时候都会将数据先暂时写入缓冲区。但造成一个结果,就是我们写的数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置,调用 flush(刷新)操作,将数据刷到设备中。

    🍃代码示例

    1. public static void main(String[] args) {
    2. try(OutputStream outputStream = new FileOutputStream("test2.txt")) {
    3. // 方法一
    4. /* outputStream.write('h');
    5. outputStream.write('e');
    6. outputStream.write('l');
    7. outputStream.write('l');
    8. outputStream.write('o'); */
    9. // 方法二
    10. String s = " hello java";
    11. outputStream.write(s.getBytes());
    12. // 把旧的文件内容清空,重新去写
    13. } catch (IOException e) {
    14. e.printStackTrace();
    15. }
    16. }

    注意:每次重新写的时候,都会把旧的文件内容清空掉,重新去写。

    但是这种原生字节流的写文件方法,用起来还是不方便。

    3.2 字符流-Writer / FileWriter

    🍃代码示例

    1. public static void main(String[] args) {
    2. try (Writer writer = new FileWriter("test2.txt")) {
    3. // 能写字符串,就很方便
    4. writer.write("hello world");
    5. } catch (IOException e) {
    6. e.printStackTrace();
    7. }
    8. }

    我们的这个字符流中的 Writer 还能写字符串,就很方便。还有一种方式 -- PrintWriter ,它提供了更丰富的写。

    PrintWriter 代码示例

    1. public static void main(String[] args) {
    2. try (OutputStream outputStream = new FileOutputStream("test2.txt")) {
    3. PrintWriter printWriter = new PrintWriter(outputStream);
    4. printWriter.println("hello");
    5. printWriter.println("你好");
    6. printWriter.printf("%d: %s\n", 1, "Java");
    7. // println 需要搭配 flush 使用
    8. printWriter.flush();
    9. } catch (IOException e) {
    10. e.printStackTrace();
    11. }
    12. }

    🍃这里要注意 PrintWrite 是自带缓冲区的,缓冲区是时候会被刷新到硬盘中呢?

    🍃1.缓冲区满了;2.显示调用 flush 方法

    所以我们在调用 println 写文件的时候,务必记得 flush 刷新。


    4.小程序示例

    🍁代码示例1

    扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件。
    1. public class applet1 {
    2. public static void main(String[] args) {
    3. Scanner scanner = new Scanner(System.in);
    4. System.out.println("请输入要扫描的路径: ");
    5. String path = scanner.next();
    6. File rootPath = new File(path);
    7. // 判断文件是否存在
    8. if (!rootPath.exists()) {
    9. System.out.println("您输入的路径不存在,无法进行扫描!");
    10. return;
    11. }
    12. System.out.println("请输入要删除文件的文件名: ");
    13. String toDelete = scanner.next();
    14. // 遍历目录,查找待删除文件
    15. dfsDir(rootPath, toDelete);
    16. }
    17. // 重点:目录递归的过程
    18. public static void dfsDir(File rootDir, String toDelete) {
    19. // 每次递归的日志
    20. try {
    21. System.out.println(rootDir.getCanonicalPath());
    22. } catch (IOException e) {
    23. e.printStackTrace();
    24. }
    25. // rootDir 对象代表的目录下的所有文件名
    26. File[] files = rootDir.listFiles();
    27. if (files == null) {
    28. // 空目录,直接返回
    29. return;
    30. }
    31. // 遍历每个文件
    32. for (File file : files) {
    33. if (file.isDirectory()) {
    34. // 是目录,继续递归
    35. dfsDir(file, toDelete);
    36. } else {
    37. tryDelete(file, toDelete);
    38. }
    39. }
    40. }
    41. public static void tryDelete(File file, String toDelete) {
    42. if (file.getName().contains(toDelete)) {
    43. try {
    44. System.out.println("是待删除文件吗?(Y/N)" + file.getCanonicalPath());
    45. Scanner scanner = new Scanner(System.in);
    46. String choice = scanner.next();
    47. if (choice.equals("Y")) {
    48. file.delete();
    49. }
    50. } catch (IOException e) {
    51. e.printStackTrace();
    52. }
    53. }
    54. }
    55. }

    🍁代码示例2

    进行普通文件的复制(regular file)

    1. public class applet2 {
    2. public static void main(String[] args) {
    3. Scanner scanner = new Scanner(System.in);
    4. System.out.println("请输入待复制的文件路径: ");
    5. String srcPath = scanner.next();
    6. File srcFile = new File(srcPath);
    7. if(!srcFile.exists()) {
    8. System.out.println("待复制的文件不存在!");
    9. return;
    10. }
    11. if(!srcFile.isFile()) {
    12. System.out.println("待复制的不是普通文件!");
    13. return;
    14. }
    15. System.out.println("请输入要复制到的目标路径: ");
    16. String destPath = scanner.next();
    17. File destFile = new File(destPath);
    18. if(destFile.exists()) {
    19. System.out.println("待复制的文件在目标路径下已存在!");
    20. return;
    21. }
    22. // 以上都是准备工作
    23. // 进行拷贝工作
    24. try (InputStream inputStream = new FileInputStream(srcFile)) {
    25. try(OutputStream outputStream = new FileOutputStream(destFile)) {
    26. while(true) {
    27. byte[] buf = new byte[1024];
    28. int len = inputStream.read(buf);
    29. if(len == -1) {
    30. break;
    31. }
    32. // 写具体长度,不要写数组
    33. outputStream.write(buf,0,len);
    34. }
    35. }
    36. } catch (IOException e) {
    37. e.printStackTrace();
    38. }
    39. System.out.println("复制完成!");
    40. }
    41. }

    问题1

    while 循环里头,为什么读文件的时候,每次都知道接着上次读的地方继续往下读?

    在读文件的时候,文件对象内部,有一个 "光标" ,通过这个 "光标" 表示当前文件读到哪个位置了。每次读操作,都会让 "光标" 往后移动,一直到文件末尾,再继续读的话,就会读到一个特殊的字符 -- EOFend of file),就表示文件读完了。

    问题2

    while 循环里头,写文件的时候,为什么不直接将 buf 数组写进去,而是写具体的长度?

    举个极端的例子,我们待复制的文件大小为 2049 个字节,我们前两次读到数组里面的都是 1024,都可以把数组读满,所以将整个数组写进去是没有影响的,但是最后的一个字节,读到数组里,这时候返回的 len 是 1 ,我们此时再读数组就不合适了。

    问题3

    前面不是演示过例子,第二次写会将第一次的写清空,再写吗?

    之前是运行一次程序,写一次,再写第二次的时候,我们已经将流关闭了。而这里是读和写同时进行的,再读完文件之前,流一直都是打开的,所以不会清空。

    🍁代码示例3 

    扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)。

    1. public class TestDemo1 {
    2. public static void main(String[] args) throws IOException {
    3. Scanner scanner = new Scanner(System.in);
    4. System.out.println("请输入要搜索的目录: ");
    5. String path = scanner.next();
    6. File rootPath = new File(path);
    7. if(!rootPath.exists()) {
    8. System.out.println("要扫描的目录不存在!");
    9. return;
    10. }
    11. if(!rootPath.isDirectory()) {
    12. System.out.println("要扫描的路径不是目录!");
    13. return;
    14. }
    15. System.out.println("请输入你要搜索的关键词: ");
    16. String toFind = scanner.next();
    17. // 递归遍历目录
    18. dfsDir(rootPath, toFind);
    19. }
    20. public static void dfsDir(File rootDir, String toFind) throws IOException {
    21. File[] files = rootDir.listFiles();
    22. if(rootDir == null) {
    23. return;
    24. }
    25. for(File file : files) {
    26. if(file.isDirectory()) {
    27. dfsDir(file, toFind);
    28. } else {
    29. toFindInFile(file, toFind);
    30. }
    31. }
    32. }
    33. public static void toFindInFile(File file, String toFind) throws IOException {
    34. // 1.判断查找的关键词是否为文件的一部分
    35. if(file.getName().contains(toFind)) {
    36. System.out.println("找到文件匹配的文件,该文件路径为: " + file.getCanonicalPath());
    37. return;
    38. }
    39. // 2.判断查找的关键词是否为文件内容的一部分
    40. try (InputStream inputStream = new FileInputStream(file)) {
    41. // 这些流对象,在打开文件的时候,参数不光可以放字符串构造的路径,还可以放构造好的文件对象
    42. StringBuilder stringBuilder = new StringBuilder();
    43. Scanner scanner = new Scanner(inputStream);
    44. while(scanner.hasNextLine()) {
    45. stringBuilder.append(scanner.nextLine());
    46. }
    47. // 读取完毕,进行判断
    48. if(stringBuilder.indexOf(toFind) != -1) {
    49. System.out.println("找到文件内容匹配的文件了,其路径为: " + file.getCanonicalPath());
    50. return;
    51. }
    52. }
    53. }
    54. }

    🍃1.此处没有 contains 方法,indexOf:也可以用来查找字符串,查找到了,就返回该字符串起始位置的下标,没有查找到,就返回 -1。

    🍃2.上述代码只适合文件比较小,比较少的场景,因为 indexOf 本身的时间复杂度就是 O(M(字符串长度) * N(文件内容的长度)),如果在算上文件个数 K,那么它的时间复杂度就相当的高了:O(M*N*K)。


     

    本篇博客就到这里了,谢谢观看!! 

  • 相关阅读:
    2022谷粒商城学习笔记(二十五)支付宝沙箱模拟支付
    Android 查看手机的当时电量
    java117-list迭代器和包含方法
    【MySQL8入门到精通】基础篇- Linux系统静默安装MySQL,跨版本升级
    PT2035(TWS 蓝牙耳机双触控双输出 IC)
    招聘网站实现
    uniapp使用plus.sqlite实现图片、视频缓存到手机本地
    深度强化学习与APS的一些感想
    十大开源机器人 智能体
    【题解】蒙德里安的梦想
  • 原文地址:https://blog.csdn.net/xaiobit_hl/article/details/126134250