【Java 编程】文件操作,文件内容的读写—数据流

2年前 (2022) 程序员胖胖胖虎阿
365 0 0

一、认识文件

1、什么是文件

平时说的文件一般都是指存储在硬盘上的普通文件
形如 txt, jpg, mp4, rar 等这些文件都可以认为是普通文件,它们都是在硬盘上存储的
在计算机中,文件可能是一个广义的概念,就不只是包含普通文件,还可以包含目录 (把目录称为目录文件)

操作系统中,还会使用文件来描述一些其他的硬件设备或者软件资源

  • 网卡,操作系统中就把网卡这样的硬件设备也给抽象成了一个文件 => 简化开发
  • 显示器/键盘操作系统也是把这些设备视为文件

这里我们讨论的文件,主要还是针对普通文件来讨论
后面去学习一些其他的硬件设备对应的文件,其实也是通过类似的代码来实现操作的

普通文件是保存在硬盘上的

机械硬盘的基本构造

1.盘片,存储数据的介质

2.磁头

机械硬盘一旦上电,里面的盘片就会高速运转,例如7200转 / m
磁头就在盘片上找到对应的数据
受限于机械硬盘的硬件结构,盘片转速越高,读写速度就越快。但是因为工艺的限制,盘片的转速也不可能无限高,机械硬盘的读写速度,已经有10年停滞未前,机械硬盘都是往大容量的方向发展,
这里的这个读写速度就比内存读写慢很多 (3-4) 数量级,

后面就又有了固态硬盘 (SSD),固态硬盘 (大号U盘 / flash芯片) 的硬件结构,和机械硬盘截然不同,
固态硬盘的读写速度要比机械硬盘高很多
像最好的固态硬盘,读写速度已经接近于十几年前的内存水平了

当前课堂上讨论的硬盘以机械硬盘为主 (暂时不考虑SSD)
企业中使用的服务器还是以机械硬盘为主 (SSD要比机械硬盘也贵好几倍)


2、文件的分类

站在程序猿的角度,主要把文件给分成两类

  1. 文本文件

里面存储的是字符

文本文件本质上也是存字节的。但是文本文件中,相邻的字节在一起正好能构成一个个的字符

  1. 二进制文件

存储的是字节

这种的话字节和字节之间就完全没啥关系了

针对这两种文件,在编程的时候会存在差异

判定一个文件是文本和二进制,一种简单的方法:
记事本打开,如果打开之后是乱码,就是二进制;不是乱码就是文本。

// 文本文件:
hello

// 二进制文件:?]n{v時+.豇''&??闟悴%&掽榦ip[{qⅴu鈢	逩)钞绨奔B?N檦觀D+E))Y矓漨???Vǖ8Xv鄏輡t}6乮汭?s弭賀釣哂!9銣畻龢漶?[j8澟*|P?〓泂qt?Ze,t荬3嶿.XV|;瘬|Sy桔"窰⑶s弁??捋宄{蹫措劎+Qh缭dQ汯 e贁鼙M(?qy睸??r笋殚睩瘝?99{v4?q秃薟艍阓?躹爷摝?%biK仙??桵祹矍#}?歬浙k?掠\篐蛅亲|$?益卧搷v)5?W?鰓鉳8?N%耬飵|J?4示遏耺.???
?                    H? ?   ? ?                     @A$  @ PD? 抣€Q$?? .?垟蕧,?dQ;e}櫜PF正朘啶%]佼電

类似的,像日常中使用的.txt,.c,java `都属于文本文件

.doc,.ppt,.exe,zip,class 等等都属于二进制文件
word,excel,office 系列的文件一般都是二进制的
word 这种软件保存的不是一个单纯的文本,而是一个 “富文本":文本中带有各种格式化的信息


3、目录结构

计算机里,保存管理文件,是通过操作系统中的 “文件系统" 这样的模块来负责的
文件系统中,一般是通过 “树形” 结构 来组织磁盘上的目录和文件的
这个就是数据结构中学过的树,这里的树不是二叉的,而是 N 叉

整体的文件系统,就是这种树形结构。
如果是一个普通文件,就是树的叶子节点
如果是一个目录文件,目录中就可以包含子树,这个目录就是非叶子节点。这个树每个节点上的子树都可以有 N 个,这就是一个 N 叉树了。

在操作系统中,就通过 “路径” 这样的概念,来描述一个具体文件 / 目录的位置,路径这里有两种描述风格:

  1. 绝对路径

    从树型结构的角度来看,树中的每个结点都可以被一条从根开始,一直到达的结点的路径所描述,而这种描述方式就被称为文件的绝对路径(absolute path)

    例如以盘符开头的

    D:\install\AnLinkSetup\AnLinkSetup.exe
    D:\program\jdk\bin\javac.exe

  2. 相对路径

    除了可以从根开始进行路径的描述,我们可以从任意结点出发,进行路径的描述,而这种描述方式就被
    称为相对路径(relative path),相对于当前所在结点的一条路径。

    . 或者 .. 开头的,其中 . 表示当前路径,.. 表示当前路径的父目录 (上级路径),谈到相对路径,必须要现有一个基准目录
    相对路径就是从基准目录出发,按照一个啥样的路径找到的对应文件

例如我需要问路,问问工作人员,人家的描述方式也有两种风格:

  1. 绝对路径:无论我处在哪个位置,人家都按照从大门口进来开始进行描述
    形如:进入大门,直走进入教学楼,右转,走到底,左转,走到底,再左转走到底,右手边就是
  2. 相对路径:根据我当前处在的位置,进行不同的描述方式
  • 以 D:\program\jdk\bin 为基准目录,找到 javac.exe
    ./javac.exe 此处的, . 就表示当前目录 (基准目录)

  • 还是以D:\programyjdk\bin为基准目录,找到 src.zip
    …/src.zip,.. 就表示基准目录的上一级路径 D:\program\jdk
    再从这个 D:\program\jdk 路径中去找到 src.zip 这个文件

即使是定位到一个文件,如果基准目录不同,此时相对路径也不同
例如:

  • 以 D:\programjdk 路径为基准,去找 javac.exe 相对路径 ./bin/javac.exe
  • 以 D:\program 路径为基准,去找 javac.exe 相对路径 ./jdk/bin/javac.exe

其他知识:

即使是普通文件,根据其保存数据的不同,也经常被分为不同的类型,我们一般简单的划分为文本文件
和二进制文件,分别指代保存被字符集编码的文本和按照标准格式保存的非被字符集编码过的文件

Windows 操作系统上,会按照文件名中的后缀来确定文件类型以及该类型文件的默认打开程序。但这
个习俗并不是通用的,在 OSX、Unix、Linux 等操作系统上,就没有这样的习惯,一般不对文件类型做
如此精确地分类。

文件由于被操作系统进行了管理,所以根据不同的用户,会赋予用户不同的对待该文件的权限,一般地
可以认为有可读、可写、可执行权限

Windows 操作系统上,还有一类文件比较特殊,就是平时我们看到的快捷方式(shortcut),这种文
件只是对真实文件的一种引用而已。其他操作系统上也有类似的概念,例如,软链接(soft link)等。

最后,很多操作系统为了实现接口的统一性,将所有的 I/O 设备都抽象成了文件的概念,使用这一理念
最为知名的就是 Unix、Linux 操作系统 —— 万物皆文件


二、Java 中操作文件

1、方法介绍

Java中操作文件,主要是包含两类操作:

1、文件系统相关的操作 [C语言没有,C标准库就不支持这个操作]
指的是通过 “文件资源管理器” 能够完成的一些功能

  • 列出目录中有哪些文件

  • 创建文件

  • 创建目录

  • 删除文件

  • 重命名文件

2、文件内容相关的操作

在Java中提供了一个 File 类,通过这个类来完成上述操作
首先这个 File 类就描述了一个文件 / 目录,基于这个对象就可以实现上面的功能
File 的构造方法,能够传入一个路径,来指定一个文件,这个路径可以是绝对路径也可以是相对路径,构造好对象之后,就可以通过这些方法,来完成一些具体的功能了

属性:

修饰符及类型 属性 说明
static String pathSeparator 依赖于系统的路径分隔符,String 类型的表示
static char pathSeparator 依赖于系统的路径分隔符,char 类型的表示

构造方法:

签名 说明
File(File parent, String child) 根据父目录 + 孩子文件路径,创建一个新的 File 实例
File(String pathname) 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者 相对路径
File(String parent, String child) 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用 路径表示

方法:

修饰符及返回 值类型 方法签名 说明
String getParent() 返回 File 对象的父目录文件路径
String getName() 返回 File 对象的纯文件名称
String getPath() 返回 File 对象的文件路径
String getAbsolutePath() 返回 File 对象的绝对路径
String getCanonicalPath() 返回 File 对象的修饰过的绝对路径
boolean exists() 判断 File 对象描述的文件是否真实存在
boolean isDirectory() 判断 File 对象代表的文件是否是一个目录
boolean isFile() 判断 File 对象代表的文件是否是一个普通文件
boolean createNewFile() 根据 File 对象,自动创建一个空文件。成功创建后返回 true
boolean delete() 根据 File 对象,删除该文件。成功删除后返回 true
void deleteOnExit() 根据 File 对象,标注文件将被删除,删除动作会到 JVM 运行结束时才会进行
String[] list() 返回 File 对象代表的目录下的所有文件名
File[] listFiles() 返回 File 对象代表的目录下的所有文件,以 File 对象 表示
boolean mkdir() 创建 File 对象代表的目录
boolean mkdirs() 创建 File 对象代表的目录,如果必要,会创建中间目录
boolean renameTo(File dest) 进行文件改名,也可以视为我们平时的剪切、粘贴操作
boolean canRead() 判断用户是否对文件有可读权限
boolean canWrite() 判断用户是否对文件有可写权限

2、代码演示:

import java.io.File;

文件操作,也是一种输入输出,File这个东西也就是在 lO 中了

// 绝对路径
File file1 = new File("e:/test.txt");
// 相对路径
File file2 = new File("./test.txt");

谈到相对路径,一定得先明确一个"基准路径’’
上述代码中,基准路径是啥? [光看这个代码,是看不出来]
基准路径由是啥姿势运行这个 java 程序来确定!!! (不同的运行 Java 程序的方式,这里的基准路径就不相同!! )

1)、如果通过命令行的方式 ( java Demo1),此执行命令所在的目录,就是基准路径
C:\Users\Gwen>javaE:\DevTools>java
[实际上不考虑这个情况] 毕竟咱们当前也不需要通过命令来运行 java 程序

2)、如果是通过 IDEA 的方式来运行程序,此时基准路径就是当前 java 项目所在的路径
在 IDEA中直接运行,基准路径就是 Open In — Explorer 打开的目录,此处写的 "./test.txt” 在 IDEA 中运行,意思就是找 system_code 目录下的 test.txt,—旦路径指定错了很容易出现找不到文件的情况

3)、后面还会学到,把一个 java 代码打成 war 包,放到 tomcat 上运行,这种情况下基准路径就是 tomcat 的 bin 目录 [后面再说]

import java.io.File;
import java.io.IOException;

public class Demo1 {
    public static void main(String[] args) throws IOException {
        File f = new File("e:/test.txt");
        // 获取到文件的父目录
        System.out.println(f.getParent()); // e:\
        // 获取到文件名
        System.out.println(f.getName()); // test.txt
        // 获取到文件路径 -- 构造 File 的时候指定的路径
        System.out.println(f.getPath()); // e:\test.txt
        // 获取到绝对路径
        System.out.println(f.getAbsolutePath()); // e:\test.txt
        // 获取到绝对路径
        System.out.println(f.getCanonicalPath()); // E:\test.txt

        
        File f2 = new File("./test.txt");
        System.out.println(f2.getParent()); // .
        System.out.println(f2.getName()); // test.txt
        System.out.println(f2.getPath()); // .\test.txt

        System.out.println(f2.getAbsolutePath());
        // E:\Gitee\java\JavaEE\system_code\.\test.txt
        //                     基准路径    在基准路径的基础上,又把相对路径给拼接上来了
        // . 仍然是表示当前目录也就是 system_code 这一级目录,完全可以把 . 给去掉

        System.out.println(f2.getCanonicalPath());
        // E:\Gitee\java\JavaEE\system_code\test.txt
        // 得到的是化简过的绝对路径
    }
}

/:读作"斜杠"
\∶读作"反斜杠"

windows 同时支持两种分隔符 /\ 都能被系统识别
但是 windows 默认使用的仍然是 \
你输入的时候,输入 / 或者 \ ,系统都能识别,输出的时候,一般默认都是 \ 的形式

import java.io.File;

public class Demo2 {
    public static void main(String[] args) {
        File f = new File("e:/test.txt");
        // 文件存在
        System.out.println(f.exists()); // true
        // 是否是一个目录
        System.out.println(f.isDirectory()); // false
        // 是否是一个普通文件
        System.out.println(f.isFile()); // true

        File f2 = new File("./test.txt");
        System.out.println(f2.exists()); // false
        System.out.println(f2.isDirectory()); // false
        System.out.println(f2.isFile()); // false
    }
}
import java.io.File;
import java.io.IOException;

public class Demo3 {
    public static void main(String[] args) throws IOException {
        // 文件的创建和剔除 在 system.code 下创建 test.txt
        File f = new File("./test.txt");
        System.out.println(f.exists());
        System.out.println("创建文件");
        f.createNewFile();
        System.out.println("创建文件结束");

        // 删除文件
        // deleteOnExit() -- JVM 运行完后进行
        f.delete();
    }
}
import java.io.File;

public class Demo4 {
    public static void main1(String[] args) {
        // 在 sysetm.code 下创建一个名为 aaa 的文件夹
        File f = new File("./aaa");
        f.mkdir();
        System.out.println(f.isDirectory()); // true
    }

    public static void main(String[] args) {
        // 创建目录的时候,使用 mkdir 只能创建一级目录,要想一次创建多级,需要使用 mkdirs
        File f =  new File("./aaa/bbb/ccc");
        f.mkdirs();
        System.out.println(f.isDirectory());
    }
}
import java.io.File;
import java.util.Arrays;

public class Demo5 {
    public static void main(String[] args) {
        File f = new File("./aaa");
        // 返回 File 对象代表的目录下的所有文件名
        System.out.println(f.list()); // [Ljava.lang.String;@1b6d3586
        System.out.println(Arrays.toString(f.list())); // [bbb]

        File f2 = new File("./");
        System.out.println(Arrays.toString(f2.list())); // [.idea, 2022.iml, aaa, out, src]
        // 返回 File 对象代表的目录下的所有文件,以 File 对象表示
        System.out.println(Arrays.toString(f2.listFiles())); // [.\.idea, .\2022.iml, .\aaa, .\out, .\src]
    }
}
import java.io.File;

public class Demo6 {
    public static void main(String[] args) {
        File f = new File("./aaa");
        File f2 = new File("./zzz");
        // 将文件 aaa 改成 zzz 的文件名
        f.renameTo(f2);

        System.out.println(f.canRead()); // 判断用户是否对文件有可读权限 false
        System.out.println(f.canWrite()); // 判断用户是否对文件有可写权限 false
    }
}

三、文件内容的读写 —— 数据流

1、方法介绍

文件内容:

1)打开文件;2)读文件;3)写文件;4)关闭文件

针对文件内容的读写,Java 标准库提供了一组,首先按照文件的内容,分成了两个系列:

  1. 字节流对象,针对二进制文件,是以字节为单位进行读写的

    读: InputStream
    写: OutputStream

  2. 字符流对象,针对文本文件,是以字符为单位进行读写的

    读: Reader
    写: Writer

这一组抽象类既可以针对普通文件的读写,也可以针对特殊文件 (网卡,socket 文件) 进行读写

以上四个都是抽象类实际使用的往往是这些类的子类

  • 字节流:
    FilelnputStream
    FileOutputStream
  • 字符流:
    FileReader
    FileWriter

这一组都是特指针对普通文件进行读写的

为啥上述内容叫做 "流” 对象?
流 Stream,这是一个形象的比喻
此处我们说的流,就像水流一样,打开开关,就源源不断的感觉

  • 例如,想通过这个水龙头,接100ml的水
    可以一次接10ml,分10次接完
    也可以一次接20ml,分5次接完
    还可以一次接100ml,分1次接

  • 例如,想通过这个流对象,来读取100个字节
    可以一次读10个字节,分10次读完
    也可以一次读20个字节,分5次读完
    还可以一次读100个字节,分1次读完

(写,也是同理)


2、InputStream

方法 :

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

read 提供了三个版本的重载

  1. 无参数版本:一次读一个字节,返回值是读到的这个字节
  2. 一个参数版本:一次读若干个字节,把读的结果放到参数中指定的数组中,返回值就是读到的字节数
  3. 三个参数版本:一次读若干个字节,把读的结果放到参数中指定的数组中,返回值就是读到的字节数
    不是从数组的起始位置放置,而是从中间位置放置 (off 这个下标的位置) len示最多能放多少个元素 (字节)

问题:

一次返回一个字节,不是应该返回一个 byte 嘛,为什么是 int 呢?

public abstract int read() throws IOException;

一个字节的范围 0 -> 255 , -128 -> +127

如果返回的是 byte,本身就是 -128 -> +127
当读出一个 -1 的时候,你到底是读到文件末尾了,还是说正好有个字节,就是 -1 这个值 (0xff)
为了表示这个非法状态,就约定了 -1 来表示,因此就需要使用一个比 byte 更大的范围,short 或者 int 都行
针对字符流,也是有类似的设定,一次读一个 char
因此此处使用 int 就会更合适一些,可以让字符流和字节流统一起来

异常:

		} catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

FileNotFoundException 这个异常是 IOException 的子类,所以也可以把这两个 catch 给合并起来,IO 操作失败的可能性是非常非常大的,例如把硬盘拔了下来,另外硬盘也容易出现 “坏道”
继承,表达的语义就是 is-a
FileNotFoundException 继承自 IOException,FileNotFoundException 也就是一个 IOException

代码1,read 读一个字节:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Demo7 {
    public static void main(String[] args) {
        // 构造方法中需要指定打开文件的路径
        // 此处的路径可以是绝对的,也可以是相对路径,还可以是 File 队形
        try {
            // 1、创建对象,同时也是在打开文件
            InputStream inputStream = new FileInputStream("e:/test.txt");
            // 2、尝试一个一个字节地读,把整个文件都读完
            while (true) {
                int b = inputStream.read();
                if (b == -1) {
                    // 读到了文件的末尾
                    break;
                }
                System.out.println(b);
            }
            // 3、读完之后记得关闭文件,释放资源
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/* 文件中的信息是 abcdef
此处读出来的这些数字,就是每个字符的 ascii 码值由于这些英文字符,本身就是一个字节的
这里按照字节读取的效果就是如此
97
98
99
100
101
102 */

**代码存在的问题:**如果在执行 read 过程中抛出异常了,就可能导致 close 关闭不了了,所以我们在 finally 中关闭

public class Demo7 {
    public static void main(String[] args) {
        InputStream inputStream = null;
        try {
            // 1、创建对象,同时也是在打开文件
            inputStream = new FileInputStream("e:/test.txt");
            // 2、尝试一个一个字节地读,把整个文件都读完
            while (true) {
                int b = inputStream.read();
                if (b == -1) {
                    // 读到了文件的末尾
                    break;
                }
                System.out.println(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3、读完之后记得关闭文件,释放资源
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

改进之后,代码是稳妥了,但是整体显的就太啰嗦
Java 中提供了一个语法,try with resourcestry ( )

public class Demo7 {
    public static void main(String[] args) {
        try (InputStream inputStream = new FileInputStream("e:/test.txt")) {
            while (true) {
                int  b = inputStream.read();
                if (b == -1) {
                    System.out.println(b);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个代码中,没有显式的调用 close,但是 try 会帮我们自动调用,当代码执行完这里的 try 语句块之后,就会自动的调用 close

得符合一定的条件,才能放到 try()中,实现 Closeable 这个 interface,所有的流对象,都实现了 Closeable,所以就可以直接放了

很多语言都有这种语法风格,C++ 中的 scoped_ptr
Go 中的 defer
Python 中的 with
都是起到这个类似的效果的

代码2,read 若干个字节:

import java.io.InputStream;

public class Demo7 {
    public static void main(String[] args) {
        try (InputStream inputStream = new FileInputStream("e:/test.txt")) {
            while (true) {
                byte[] buffer = new byte[1024];
                int len = inputStream.read(buffer);
                if (len == -1) {
                    return;
                }
                for (int i = 0; i < len; i++) {
                    System.out.print(buffer[i] + " ");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 97 98 99 100 101 102 
    }
}

byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);

这个操作是把读出来的结果放到 buffer 这个数组中了,相当于是使用参数来表示方法的返回值
这种做法称为 “输出型参数”
这种操作在 Java 中比较少见,C++ 中遍地都是

每次读磁盘都是比较低效的操作,能一次多读点是更好的


3、OutputStream

方法:

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

OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream

代码1,使用字节流,写文件:

import java.io.IOException;
import java.io.OutputStream;

public class Demo8 {
    public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("e:/test.txt")) {
            /*outputStream.write(97);
            outputStream.write(98);
            outputStream.write(99);*/
            byte[] buffer = new byte[] {97, 98, 99};
            outputStream.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

每次按照写方式打开文件,都会清空原有文件的内容,清空旧的内容,再从起始位置往后写的
如果想让打开之后不清空,从文件末尾继续往后写,FileOutputStream 设置为 true,此次运行两次,文件中内容为 abcabc

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class Demo8 {
    public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("e:/test.txt", true)) {
            byte[] buffer = new byte[] {97, 98, 99};
            outputStream.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码2,按照字符来读:

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

// 按照字符来读写
public class Demo9 {
    public static void main(String[] args) {
        try (Reader reader = new FileReader("e:/test.txt")) {
            while (true) {
                char[] buffer = new char[1024];
                int len = reader.read(buffer);
                if (len == -1) {
                    break;
                }
                /*for (int i = 0; i < len; i++) {
                    System.out.println(buffer[i]);
                }*/
                // 如果这里传入的数组是 byte 数组,还可以手动指定以下 utf8 字符集,避免乱码
                // String s = new String(buffer, 9, len, "utf-8");
                String s = new String(buffer, 0, len);
                System.out.println(s); // a bc
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码1,按照字符来写:

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class Demo10 {
    public static void main(String[] args) {
        try (Writer write = new FileWriter("e:/test.txt")) {
            write.write("xyz");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4、利用 Scanner 进行字符读取

上述例子中,我们看到了对字符类型直接使用 InputStream 进行读取是非常麻烦且困难的,所以,我
们使用一种我们之前比较熟悉的类来完成该工作,就是 Scanner 类。

构造方法 说明
Scanner(InputStream is, String charset) 使用 charset 字符集进行 is 的扫描读取

传入 FileInputStream 对象

import java.io.*;
import java.util.*;

// 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容
public class Main {
    public static void main(String[] args) throws IOException {
        try (InputStream is = new FileInputStream("hello.txt")) {
            try (Scanner scanner = new Scanner(is, "UTF-8")) {
                while (scanner.hasNext()) {
                    String s = scanner.next();
                    System.out.print(s);
                }
            }
        }
    }
}

5、利用 PrintWriter 找到我们熟悉的方法

上述,我们其实已经完成输出工作,但总是有所不方便,我们接来下将 OutputStream 处理下,使用 PrintWriter 类来完成输出,因为
PrintWriter 类中提供了我们熟悉的 print/println/printf 方法

OutputStream os = ...;
OutputStreamWriter osWriter = new OutputStreamWriter(os, "utf-8"); 

// 告诉它,我们的字符集编码是 utf-8 的
PrintWriter writer = new PrintWriter(osWriter);

// 接下来我们就可以方便的使用 writer 提供的各种方法了
writer.print("Hello");
writer.println("你好");
writer.printf("%d: %s\n", 1, "没什么");

// 不要忘记 flush
writer.flush();

示例:

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        try (OutputStream os = new FileOutputStream("output.txt")) {
            try (OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8")) {
                try (PrintWriter writer = new PrintWriter(osWriter)) {
                    writer.println("我是第一行");
                    writer.print("我的第二行\r\n");
                    writer.printf("%d: 我的第三行\r\n", 1 + 1);
                    writer.flush();
                }
            }
        }
    }
}

.flush 叫做"刷新缓冲区"

缓冲区:
例如,嗑瓜子,抓了一把瓜子皮的手,就是 “缓冲区”。
输出缓冲区 (放瓜子皮的手)
输入缓冲区 (放瓜子的手)
缓冲区存在的意义就是为了提高效率,在计算机中尤其重要。CPU读取内存的速度大大高于硬盘
例如需要写数据到硬盘上,与其一次写一点,分多次写,不如把一些数据攒一堆,统一一次写完(这一堆数据就是在内存中保存的,这块内存就叫缓冲区)。

读操作也是类似,与其一次读一点,分多次读,不如一次性的读一堆数据,然后再慢慢消化(这块内存也是缓冲区)。

例如写数据的时候,需要把数据先写到缓冲区里,然后再统一写硬盘。
如果当前缓冲区已经写满了,就直接触发写硬盘操作,
如果当前缓冲区还没满,也想提前写硬盘,就可以通过 flush 来手动 “刷新缓冲区”
咱们前面的代码,没涉及到 flush,原因是当前这些代码直接都很快的就涉及到close操作,close 也会触发缓冲区刷新。


三、小程序练习

1、查找删除文件

扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件

文件系统上的目录,是一个树形结构。二叉树的遍历,就有四种常见的:先序、中序、后序、层序。
N 叉树,同样也是通过递归的方式来进行遍历的。

package file;

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

// 案例1:查找删除文件
public class Demo11 {
    public static void main(String[] args) {
        // 1、先输入要扫描的目录,以及要删除的内容
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要扫描的路径:");
        String rootDirPath = scanner.next();
        System.out.println("请输入要删除的文件名:");
        String toDeleteName = scanner.next();
        File rootDir = new File(rootDirPath);
        if (!rootDir.isDirectory()) {
            System.out.println("输入的扫描路径有误!");
            return;
        }
        // 2、遍历目录,把 指定目录 中的所有文件和子目录都遍历一遍,从而找到要删除的文件
        //    通过这个方法来实现 递归和遍历并删除 的操作
        scanDir(rootDir, toDeleteName);
    }

    private static void scanDir(File rootDir, String toDeleteName) {
        // 1、列出 rootDir 中有哪些内容
        File[] files = rootDir.listFiles();
        if (files == null) {
            // rootDir 是一个空目录
            return;
        }
        // 2、遍历当前列出的这些内容,如果是普通文件,就检测文件名时候是要删除的文件,
        //    如果是目录,就递归地调用进行遍历
        for (File f : files) {
            if (f.isFile()) {
                // 不要求名字完全一样,只要文件名中包含了关键字即可删除
                deleteFile(f);
            } else if (f.isDirectory()) {
                // 是目录就递归
                scanDir(f, toDeleteName);
            }
        }
    }

    private static void deleteFile(File f) {
        try {
            System.out.println(f.getCanonicalPath() + " 确认要删除吗(Y/N)");
            Scanner scanner = new Scanner(System.in);
            String choice = scanner.next();
            if (choice.equals("Y") ||  choice.equals("y")) {
                f.delete();
                System.out.println("文件删除成功!");
            } else {
                System.out.println("文件放弃删除!");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2、普通文件的复制

需要让用户指定两个文件路径,一个是源路径 (被复制的文件)
一个是目标路径 (复制之后生成的文件)
打开源路径的文件,读取里面的内容,并写入到目标文件

import java.io.*;
import java.util.Scanner;

public class Demo12 {
    public static void main(String[] args) {
        // 1、输入两个路径
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要拷贝的路径:");
        String scr = scanner.next();
        System.out.println("请输入要拷贝的目标路径:");
        String dest = scanner.next();
        File scrFile = new File(scr);
        if (!scrFile.isFile()) {
            System.out.println("输入的源路径不正确!");
            return;
        }
        // 此处不太需要检查目标文件是否存在,OutputStream 写文件的时候,能够自动创建不存在的文件
        // 2、读取源文件,拷贝到目标文件中
        try (InputStream inputStream = new FileInputStream(scr)) {
            try (OutputStream outputStream = new FileOutputStream(dest)) {
                // 把 inputStream 中的数据读出来,写入到 outputStream 中
                byte[] buffer = new byte[1024];
                while (true) {
                    int len = inputStream.read(buffer);
                    if (len == -1) {
                        // 读取完毕
                        return;
                    }
                    // 写入的时候,不能把整个 buffer 都进去,可能 buffer 只有一部分是有效数据,只写入读出来的
                    outputStream.write(buffer, 0, len);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/* 请输入要拷贝的路径:
e:/test.txt
请输入要拷贝的目标路径:
e:/test2.txt

请输入要拷贝的路径:
e:/picture.png
请输入要拷贝的目标路径:
e:/picture2.png */

3、文件内容查找

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

先输入一个路径,再输入一个要查找的文件内容的 “关键词”
递归的遍历文件,找到看哪个文件里的内容包含了关键词,就把对应的文件路径打印出来
先递归遍历文件,针对每个文件都打开,并读取内容,再进行字符串查找即可

注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下实验,要想实现更高效的全文检索,需要倒排索引这样的数据结构

package file;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Scanner;

public class Demo13 {
    public static void main(String[] args) throws IOException {
        // 1、输入要扫描的文件路径
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要扫描的路径:");
        String rootDirPath = scanner.next();
        System.out.println("请输入要查询的关键词:");
        String word = scanner.next();
        File rootDir = new File(rootDirPath);
        if (!rootDir.isDirectory()) {
            System.out.println("输入的路径非法!");
            return;
        }
        // 2、递归地遍历
        scanDir(rootDir, word);
    }

    private static void scanDir(File rootDir, String word) throws IOException {
        // 1、先列出 rootDir 中都有哪些内容
        File[] files = rootDir.listFiles();
        if (files == null) {
            return;
        }
        // 2、遍历每个元素,针对普通文件和目录分别进行处理
        for (File f : files) {
            if (f.isFile()) {
                // 针对文件 内容查找
                if (containsWord(f, word)) {
                    System.out.println(f.getCanonicalPath());
                }
            } else if (f.isDirectory()) {
                // 针对目录 递归
                scanDir(f, word);
            }
        }
    }

    private static boolean containsWord(File f, String word) {
        // 把 f 中的内容都读出来,放到一个 StringBuilder 中
        StringBuilder stringBuilder = new StringBuilder();
        try (Reader reader = new FileReader(f)) {
            char[] buffer = new char[1024];
            while (true) {
                int len = reader.read(buffer);
                if (len == -1) {
                    break;
                }
                // 把一个段读到的结果,放到 StringBuilder 中
                stringBuilder.append(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // index 返回的是子串的下标,如果 word 在 StringBuilder 中存在,返回下标为 -1
        return stringBuilder.indexOf(word) != -1;
    }
}

相关文章

暂无评论

暂无评论...