7、File类与I/O流


一、File 文件/夹操作

在计算机系统中,文件是非常重要的存储方式。Java 的标准库java.io提供了File对象来操作文件和目录。
要构造一个File对象,需要传入文件路径:

public class Main {
    public static void main(String[] args) {
        File f = new File("C:\\Windows\\notepad.exe");
        System.out.println(f);	//C:\Windows\notepad.exe
    }
}

构造 File 对象时,既可以传入绝对路径,也可以传入相对路径。绝对路径是以根目录开头的完整路径,例如:

File f = new File("C:\\Windows\\notepad.exe");

注意 Windows 平台使用\作为路径分隔符,在 Java 字符串中需要用\\表示一个\。Linux 平台使用/作为路径分隔符:

File f = new File("/usr/bin/javac");

传入相对路径时,相对路径前面加上当前目录就是绝对路径:

// 假设当前目录是C:\Docs
File f1 = new File("sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File("..\\sub\\javac"); // 绝对路径是C:\sub\javac

可以用.表示当前目录,..表示上级目录。
File 对象有 3 种形式表示的路径,一种是getPath(),返回构造方法传入的路径,一种是getAbsolutePath(),返回绝对路径,一种是getCanonicalPath,它和绝对路径类似,但是返回的是规范路径。
什么是规范路径?我们看以下代码:

public class Main {
    public static void main(String[] args) throws IOException {
        File f = new File("..");
        System.out.println(f.getPath());	//..
        System.out.println(f.getAbsolutePath());	//  /app/..
        System.out.println(f.getCanonicalPath());   //  /
    }
}

绝对路径可以表示成C:\Windows\System32\..\notepad.exe,而规范路径就是把...转换成标准的绝对路径后的路径:C:\Windows\notepad.exe
因为 Windows 和 Linux 的路径分隔符不同,File 对象有一个静态变量用于表示当前平台的系统分隔符:

System.out.println(File.separator); // 根据当前平台打印"\"或"/"

1.文件和目录

File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。
例如,调用isFile(),判断该File对象是否是一个已存在的文件,调用isDirectory(),判断该File对象是否是一个已存在的目录:

public class Main {
    public static void main(String[] args) throws IOException {
        File f1 = new File("C:\\Windows");
        File f2 = new File("C:\\Windows\\notepad.exe");
        //是文件?
        System.out.println(f1.isFile());		//false
        //是目录?
        System.out.println(f1.isDirectory());	//false
        System.out.println(f2.isFile());//false
        System.out.println(f2.isDirectory());//false
    }
}

File对象获取到一个文件时,还可以进一步判断文件的权限和大小:

  • boolean canRead():是否可读;
  • boolean canWrite():是否可写;
  • boolean canExecute():是否可执行;
  • long length():文件字节大小。

对目录而言,是否可执行表示能否列出它包含的文件和子目录。

2.创建和删除文件

当 File 对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件:

File file = new File("/path/to/file");
file.createNewFile()
file.delete()

有些时候,程序需要读写一些临时文件,File 对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在 JVM 退出时自动删除该文件。

File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除
System.out.println(f.isFile());		//true
System.out.println(f.getAbsolutePath());	///tmp/tmp-11900775370391459620.txt

3.遍历文件和目录

当 File 对象表示一个目录时,可以使用list()listFiles()列出目录下的文件和子目录名。listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录:

public class Main {
    public static void main(String[] args) throws IOException {
        File f = new File("C:\\Windows");
        File[] fs1 = f.listFiles(); 	// 列出所有文件和子目录
        printFiles(fs1);
        File[] fs2 = f.listFiles(new FilenameFilter() { 	// 仅列出.exe文件
            public boolean accept(File dir, String name) {
                return name.endsWith(".exe"); // 返回true表示接受该文件
            }
        });
        printFiles(fs2);
    }
    static void printFiles(File[] files) {
        System.out.println("==========");
        if (files != null) {
            for (File f : files) {
                System.out.println(f);
            }
        }
        System.out.println("==========");
    }
}

和文件操作类似,File 对象如果表示一个目录,可以通过以下方法创建和删除目录:

  • boolean mkdir():创建当前 File 对象表示的目录;
  • boolean mkdirs():创建当前 File 对象表示的目录,并在必要时将不存在的父目录也创建出来;
  • boolean delete():删除当前 File 对象表示的目录,当前目录必须为空才能删除成功。

4.Path

Java 标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单:

Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
System.out.println(p1);
Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
System.out.println(p2);
Path p3 = p2.normalize(); // 转换为规范路径
System.out.println(p3);
File f = p3.toFile(); // 转换为File对象
System.out.println(f);
for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
    System.out.println("  " + p);
}

如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。
小结:
Java 标准库的java.io.File对象表示一个文件或者目录:

  • 创建File对象本身不涉及 IO 操作;
  • 可以获取路径/绝对路径/规范路径:getPath()/getAbsolutePath()/getCanonicalPath()
  • 可以获取目录的文件和子目录:list()/listFiles()
  • 可以创建或删除文件和目录。

5.练习

请利用File对象列出指定目录下的所有子目录和文件,并按层次打印。
例如,输出:

Documents/
  word/
    1.docx
    2.docx
    work/
      abc.doc
  ppt/
  other/

如果不指定参数,则使用当前目录,如果指定参数,则使用指定目录。

6.总结

6.1 文件句柄方法

File file = new File("c:/qf/abc/小姐姐.txt");
//操作
file.createNewFile();// 创建文件
file.getParentFile();// 获取到上级目录句柄
file.getParent();	//获取到上级目录字符串
file.mkdir();// 创建文件夹
file.mkdirs();// 创建文件夹(多级文件夹,推荐!!!)
file.renameTo(new File("新名字"));// 重命名
file.delete();// 删除文件
// 查看
System.out.println(file.exists()); // 判断文件是否存在
System.out.println(file.isAbsolute()); // 是否是绝对路径
System.out.println(file.isDirectory()); // 是否是文件夹
System.out.println(file.isFile()); // 是否是文件
System.out.println(file.getName()); //获取文件名字
System.out.println(file.length()); // 文件大小

6.2 创建文件的步骤

File file = new File("c:/qf/abc/小姐姐.txt");
// 首先, 判断文件夹是否存在
File parentFile = file.getParentFile();
if(!parentFile.exists() || parentFile.isFile() ){
    parentFile.mkdirs();// 创建上级目录
}
file.createNewFile(); // 创建文件

二、基本 IO 流

上一节, 我们可以创建文件和删除文件, 也就是说, 我们可以对文件进行操作了. 但是文件的内容还没办法读取和写入. 本节, 咱们就说说这个文件怎么读取和写入. 在 java 中读写文件使用 IO 流来完成.
流的分类:

  1. 按照读写内容的单位来讲, 分为字节流和字符流
  2. 按照读写的方向来讲, 分为输入流和输出流
  3. 按照流的功能不同, 分为节点流和处理流

懵逼了吧. 哈哈. 别担心. 我们一个一个看. 1.站在程序的角度. 从文件里读取内容,这个叫输入流,向文件里写入内容叫输出流。 2.对于英文而言我们之前说过, 英文是 ascii 的范畴。一个 ascii 是一个字节。所以字节流一次读取的是一个字节,这样的流是可以处理英文的,但是如果是中文就不行了,中文必须用字符流。 3.节点流主要指的是直接插在文件上的流,在文件上开辟一个节点来读取内容。而处理流是用来处理其他流的,就好比我们家的自来水净化器,它是套在我们正常的自来水管道上的。我们从净化器接的水是被净化器处理过的,一样. 从处理流获取到的数据是经过处理的。
接下来,我们来说说流的家族体系。不管是什么流,最终都是基于这个体系产生的。
流一共有 4 个祖宗.

输入 输出
字节流 InputStream OutputStream
字符流 Reader Writer

万变不离其宗, 所有的流在使用上基本上都是从这几个流过来的. 但是很不幸, 这几个流都是抽象类. 我们不能直接创建这几个类的对象. 所以, 我们必须学习他们的子类间接的来学习流。
节点流:文件流

  1. FileInputStream
  2. FileOutputStream
  3. FileReader
  4. FileWriter

1.文件字节输入流FileInputStream

public static void main(String[] args) {
    try {
        //1.以字节流形式打开文件
        InputStream fis = new FileInputStream(new File("aa.txt"));
        //每次读取一个字节
        //            int b = fis.read();     //每次读取1个字节
        //            System.out.println(b);  //第1个字节 在ASCII中数字表示
        //            System.out.println((char)b);
        //            int b2 = fis.read();     //每次读取2个字节
        //            System.out.println(b2);  //第2个字节 在ASCII中数字表示
        //            System.out.println((char)b2);
        //2.读取一定长度字节,read() 传入一定长度的字节数组
        byte[] bs = new byte[1024];
        int len=0;
        while ((len = fis.read(bs)) != -1){ // 当文件内容读完之后 fis.read() == -1
            System.out.println(new String(bs,0,len));   //  字节数组中不全是从文件里读取到的内容(如果文件内容少于1024 剩余位置用null占位)
        }
        //3.关闭文件
        fis.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2.文件字节输出流FileOutputStream

构造方法

成员方法

public static void main(String[] args) {
    try {
        //准备工作-创建文件
        File fw = new File("./notebooks/aa.txt");
        File parentFile = fw.getParentFile();
        if(!parentFile.exists()){
            parentFile.mkdirs();
        }
        fw.createNewFile();
        //1.以字节流形式打开文件
        OutputStream fos = new FileOutputStream(fw);
        //OutputStream fos = new FileOutputStream("./1.jpg");	//可以直接传入文件路径
        //2.读取一定长度字节,read() 传入一定长度的字节数组
        byte[] bs = "我是中国人,黄皮肤,棕色眼睛,我是龙的传人".getBytes();
        fos.write(bs);
        //3.写入文件
        fos.flush();
        fos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

我们发现, 中文可以写出去. 原因是, 我们按照字节的形式写出去. 到了文件里. 又被拼凑成正常的文字了. 所以可以看见中文. 但是 read 就不行了. read 读到的是字节. 对于 GBK 而言, 你每次读取的其实就是半个字. 没办法显示的. 先更要读取中文. 必须用字符流

3.文件字符输入流FileReader

构造方法

  • FileReader(File file) 创建一个新的 FileReader ,给出要读取的 File对象。
  • FileReader(String fileName) 创建一个新的 FileReader ,给定要读取的文件名称。

成员方法

  • close() 关闭流并释放与之相关联的任何系统资源
  • int len = read() 读一个字符
  • int len = read(char[] cbuf, int offset, int length) 将字符读入数组的一部分。
public static void main(String[] args) {
    try {
        //1.以字符形式打开文件
        Reader file_reader = new FileReader(new File("aa.txt"));
        //2.读取一定长度字节,read() 传入一定长度的字符数组
        char[] temp = new char[1024];
        int len=0;
        while ((len = file_reader.read(temp)) != -1){ // 当文件内容读完之后 fis.read() == -1
            System.out.println(new String(temp,0,len));   //  字符流能读取中文
        }
        //3.关闭文件
        file_reader.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

聪明的你应该已经发现了. 这个 Reader 和 InputStream 的代码很像. 除了 byte 和 char 数组的区别. 其他写法一模一样. 这就对了. 这两个功能本来就是一样的. 区别就是单位的不同. 所以. 关于 IO 流的代码. 记住一份足以. 其他的照葫芦画瓢就行了.

4.文件字符输出流FileWriter

public static void main(String[] args) {
    try {
        //准备工作-创建文件
        File fw = new File("./notebooks/aa.txt");
        File parentFile = fw.getParentFile();
        if(!parentFile.exists()){
            parentFile.mkdirs();
        }
        fw.createNewFile();
        //1.以字节流形式打开文件
        Writer file_writer = new FileWriter(fw,true);   //append 追加写入
        //2.直接写入字符串
        file_writer.write("\r\n我是下一行数据追加!");        //换行
        //3.写入文件
        file_writer.flush();
        file_writer.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

练习:使用 IO 流, 把一张图片从 aa 文件夹复制到 bb 文件夹(不存在就先创建).(附加思考: 如果是剪切呢?)

public static void main(String[] args) throws Exception {
    /*使用IO流, 把一张图片从aa文件夹复制到bb文件夹(不存在就先创建).(附加思考: 如果是剪切呢?)*/
    File f_source = new File("./aa/meinv.jpg");
    InputStream fis = new FileInputStream(f_source);
    //写byte
    File file = new File("./bb/new_meinv.jpg");
    File parentFile = file.getParentFile();
    if(!parentFile.exists()){
        parentFile.mkdirs();
    }
    file.createNewFile();
    OutputStream fos = new FileOutputStream(file);
    byte[] bs = new byte[1024];
    int len = 0;
    while ((len = fis.read(bs))!= -1){
        fos.write(bs,0,len);
    }
    //剪切也简单-拷贝完之后直接删除fis即可
    f_source.delete();
    fos.flush();
    fos.close();
    System.out.println("拷贝完成!");
}

总结: 1.文档(含中英文)操作,使用字符流FileReader-Reader FileWriter-Writer,可以直接读写字符串; 2.音视频/压缩文件等操作,使用字节流FileInputStream-InputStream FileOutputStream-OutStreamread(bs) write(bs,0,len)需要传入字节长度数组;

三、处理流

1.概念:

上节课我们还学了文件流(即节点流):FileInputStream, FileOutputStream, FileReader, FileWriter
以上流都是节点流,这节课我们来探索一下处理流。
前面我们说过, 从文件中读取内容相当于在文件上插了一个管子,在管子上进行读写数据,就好比我们喝奶茶的时候插根管子从管子里喝。

我们可以在这个管道的基础上进一步的进行处理,处理管道的叫处理流。说白了. 在管道上再套一根管道.

此时读取和写入的内容就会自动被处理流给处理一下.
处理流的分类:

  1. 缓冲流
  2. 转换流
  3. 对象流

2.缓冲流 BufferedReader

输入 输出
字节流 BufferedInputStream BufferedOutputStream
字符流 BufferedReader BufferedWriter

发现规律了么? 还是 4 个,没错你把最原始的那四个记住了. 基本上流也就掌握了.

我们发现, BufferedInputStream 除了在创建对象的时候多套弄了一层以外, 好像没有什么实质的进展. 不好用. 对于 BufferedOutputStream 其实也是一样的。所以这两个类一般不怎么使用,关键在BufferReader

public static void main(String[] args) throws Exception {
    BufferedReader bf = new BufferedReader(new FileReader(new File("麻花藤.txt")));
    //        String str = bf.readLine(); // 一次读取一行内容. 爽啊. 终于不用再管神马byte[] 了
    //        System.out.println(str);
    String str = "";
    while((str = bf.readLine())!=null){   // 读取文本文件最爽的一种写法
        System.out.println(str);
    }
}

3.转换流

我们已经可以使用流的基本操作来完成读写任务了. 但是, 还有一个事情我们需要解决. 在很多情况下. 我们是拿不到字符流的。就比如System.in. 这个就是一个字节流。它默认连接着控制台,那我们如何从 System.in 中读取到数据呢? 我么可以使用转换流,把 System.in 这种字节流转换成字符流,这样读取数据的时候就方便很多。
转换流有两个, 一个是InputStreamReader, 另一个是OutputStreamWriter

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println(br.readLine());

运行起来, 感觉和 Scanner 是一样的.
同理. System.out 是一个输出流. 我们就可以把 System.out 转换成 Writer

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
bw.write("我的天啊");
bw.newLine();		//换行
bw.flush();
bw.close();
System.out.println("hahaaha!");		//无法显示

注意, System.out 不可以关闭,关闭了 后面我们就不能再打印了

4.对象流

在 java 中, 万事万物皆为对象, 那我们能不能把一个对象写入到文件里呢? 答案是肯定的. 如何操作呢? 我们需要用到对象流ObjectStream

public static void main(String[] args) throws Exception {
    //创建对象
    Teacher t = new Teacher(1, "蔡徐坤", 88);
    // 创建对象输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("teacher.dat")));
    oos.writeObject(t); // 把一个对象写入到一个文件中
    oos.flush();
    oos.close();
}

代码写完了. 运行, 发现报错了. 为什么呢? 先看看这个错误是什么?

Exception in thread “main” java.io.NotSerializableException: com.xyq.io.Teacher
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:349)
at com.xyq.io.Test.main(Test.java:14)

NotSerializableException: 表示没有开启序列化功能。什么是序列化呢? 我们知道我们创建的对象是保存在内存里的, 数据内存的数据, 而内存和硬盘里的数据保存方式是不一样的. 对于硬盘而言, 只有两种格式, 要么是字符, 要么是字节. 就这两种保存方式, 所以, 我们想要把对象写入到硬盘, 就必须把一个对象转换成字节或者字符。
对于一个对象而言, 转换成字符很显然不是很合适, 那把一个对象转换成字节的过程被称之为序列化, 并且, 序列化的内容将来还可能会被反序列化回我们的对象. JDK 为每一个类都准备了序列化功能. 但是由于序列化太过于消耗资源. 默认都是不开启的. 如果想要开启该功能. 需要让你的类实现 Serializable 接口,这是一个标志性接口, 并没有方法在里面(即不需要重写方法),当 JDK 发现你的类实现了这个接口了,就意味着你要序列化了。所以. 大家以后只要看到了这个错误. 直接实现一个接口即可。

public class Person implements Serializable {

测试通过…来, 看看这个文件里写的是什么把

我们发现这个文件里好多都是乱码. 隐隐约约能看到蔡徐坤。所以. 记住, 这个文件不是给人看的. 是给程序用的. 那程序怎么用呢? 很简单. 你能写, 就一定能读. 对吧. 写用ObjectOutputStream. 那读一定是ObjectInputStream
Person.java

public class Person implements Serializable {
    private String name;
    private int age;
    private String gender;
    public Person(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
}

对象写入:

public static void main(String[] args) throws Exception {
    Person p1 = new Person("刘德华",18,"男");
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"));
    oos.writeObject(p1);
    oos.flush();
    oos.close();
}

对象读取:

public static void main(String[] args) throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("person.dat")));
    Object obj = ois.readObject();
    Person p1 = (Person) obj;
    System.out.println(p1.getName());	//刘德华
    ois.close();
}

四、文件修改

1.替换 txt 文件关键字

把文件”唐诗三百首.txt”文件中所有的李白替换成李太白.

唐诗三百首
1. 静夜思, 李白, 窗前明月光, 疑是地上霜, 举头望明月, 低头思故乡
2. 望庐山瀑布, 李白, 日照香炉生紫烟, 遥看瀑布挂前川, 飞流直下三千尺, 疑是银河落九天.
3. 早发白帝城, 李白, 朝辞白帝彩云, 千里江陵一日还, 两岸猿声啼不住, 轻舟已过万重山.

结果:

唐诗三百首
1. 静夜思, 李太白, 窗前明月光, 疑是地上霜, 举头望明月, 低头思故乡
2. 望庐山瀑布, 李太白, 日照香炉生紫烟, 遥看瀑布挂前川, 飞流直下三千尺, 疑是银河落九天.
3. 早发白帝城, 李太白, 朝辞白帝彩云, 千里江陵一日还, 两岸猿声啼不住, 轻舟已过万重山.

用咱们现在所知道的知识点还无法直接解决. 为什么会这样呢? 我们知道硬盘是一个连续的存储结构. 假设被写入到磁盘的是这样的:

此时我想把”叫”改成”朋友”, 那么假设可以直接修改. 就会变成这样

所以, 想要实现文件修改功能. 不可以在原有的基础上进行修改. 那怎么办? 我们可以准备一个文件的副本. 把新内容写到副本文件里. 写完之后, 把副本变成原文件. 并把源文件删除. 如果这个动作够快. 大家看到的就是文件修改的效果. 我们平时使用的 word 就是这样操作的. 所以我们每次操作 word 文档的时候都能看到一个文件副本出现. 一保存, 文件副本不见了. 其实是把文件副本变成了源文件

public static void main(String[] args) throws Exception {
    File f1 = new File("./唐诗三百首.txt");
    File f2 = new File("./副本_唐诗三百首.txt");
    BufferedReader br = new BufferedReader(new FileReader(f1));
    BufferedWriter bw = new BufferedWriter(new FileWriter(f2));
    String line = "";
    while ((line = br.readLine()) != null){
        line = line.replace("李太白","【李太白】");
        bw.write(line);     //写到新文件
        bw.newLine();    //换行
    }
    br.close();
    bw.flush();
    bw.close();
    //删除源文件
    f1.delete();
    f2.renameTo(f1);
}

2.计算水果价钱

再来看另一个案例,读取文件中的水果信息. 计算水果的总价(价格 x 库存). 并写入到文件中
fruit.txt:

名称_价格_库存
香蕉_3.33_20
苹果_1.25_10
橘子_1.33_40

结果:

名称_价格_库存_总价
香蕉_3.33_20_66.60
苹果_1.25_10_12.50
橘子_1.00_40_40.00

代码如下:

public static void main(String[] args) throws Exception {
    File file_source = new File("fruit.txt");
    BufferedReader br = new BufferedReader(new FileReader(file_source));
    File file_ret = new File("temp_fruit.txt");
    BufferedWriter bw = new BufferedWriter(new FileWriter(file_ret));
    DecimalFormat df = new DecimalFormat(".00");
    //读第一行数据
    String title = br.readLine();
    bw.write(title+"_总价");
    bw.newLine();
    String line = "";
    while ((line = br.readLine())!=null){
        String[] price_num = line.split("_");
        Double price = Double.parseDouble(price_num[1]);
        Double num = Double.parseDouble(price_num[2]);
        Double total = price*num;
        String totalStr = df.format(total);
        bw.write(line+="_"+totalStr);
        bw.newLine();
    }
    br.close();
    bw.flush();
    bw.close();
    //删除源文件并重命名新文件
    file_source.delete();
    file_ret.renameTo(file_source);
}


文章作者: CoderXiong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CoderXiong !
  目录