已复制
全屏展示
复制代码

JAVA 文件操作原理与实战总结


· 12 min read

一. JAVA数据流

IO流是一种顺序读写数据的模式,它的特点是单向流动。

1.1 字节流

  • IO流以byte(字节)为最小单位,因此也称为字节流。
  • 比如,我们要从磁盘读入一个文件,包含6个字节,就相当于读入了6个字节的数据,这6个字节是按顺序读入的,所以是输入字节流。反过来,我们把6个字节从内存写入磁盘文件,就是输出字节流。
  • 在Java中,InputStream代表输入字节流,OuputStream代表输出字节流,这是最基本的两种IO流。
  • 字节流抽象类:InputStream/OutputStream

1.2 字符流

  • 如果需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char来读写显然更方便,这种流称为字符流。
  • Java提供了Reader和Writer表示字符流,字符流传输的最小数据单位是char。
  • 假如,把char[]数组Hi你好这4个字符用Writer字符流写入文件,并且使用UTF-8编码,得到的最终文件内容是8个字节,英文字符H和i各占一个字节,中文字符你好各占3个字节
  • 反过来,我们用Reader读取以UTF-8编码的这8个字节,会从Reader中得到Hi你好这4个字符。
  • 因此,Reader和Writer本质上是一个能自动编解码的InputStream和OutputStream。
  • 字符流接口:Reader/Writer。

二. 文件对象

2.1 创建文件对象

  • 构造File对象时,既可以传入绝对路径,也可以传入相对路径。
  • File对象既可以表示文件,也可以表示目录。构造一个File对象时。只有当调用File对象的某些方法的时候,才真正进行磁盘操作。
import java.io.File;

File f = new File("C:\\Windows\\notepad.exe"); // 绝对路径
File f = new File("Documents\\Java");          // 相对路径
File f = new File(".\\Documents\\Java");       // 相对路径
File f = new File("..\\Documents\\Java");      // 相对路径

File f = new File("/home/yzyv/programs/netflow/pom.xml"); // 绝对路径
File f = new File("netflow/pom.xml");                     // 相对路径
File f = new File("./netflow/pom.xml");                   // 相对路径
File f = new File("../programs/netflow/pom.xml");         // 相对路径

System.out.println(f);

2.2 文件对象的三种路径表示

  • getPath(),返回构造方法传入的路径
  • getAbsolutePath(),返回绝对路径
  • getCanonicalPath,它和绝对路径类似,但是返回的是规范路径
File f = new File("..");
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());

// 加入在/home/yzy目录下执行程序,结果如下
..
/home/yzy/..
/home

2.3 文件对象的常用方法

加入有文件对象File f = new File("..");,文件对象的常用方法有如下:

  • boolean isFile():是否是文件;
  • boolean isDirectory(): 是否是目录;
  • boolean canRead():是否可读;
  • boolean canWrite():是否可写;
  • boolean canExecute():是否可执行;为目录时,表示能否列出它包含的文件和子目录。
  • long length():文件字节大小。
  • boolean mkdir():创建当前File对象表示的目录;
  • boolean mkdirs():递归创建当前File对象表示的目录
  • boolean createNewFile():创建文件
  • boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。

2.4 临时文件


// 临时文件创建时指定前缀和后缀
File file = File.createTempFile("tmp-", ".txt");

// 临时文件在JVM退出时自动删除
file.deleteOnExit();

2.5 遍历目录

下列代码表示找出C:\\Windows目录下以.exe结尾的文件或目录。

import java.io.File;
import java.io.FilenameFilter;

public class Main {
    public static void main(String[] args) throws Exception {
        File file = new File("C:\\Windows");
        File[] fs = file.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".exe");
            }
        });
        for (File f : fs) {
            System.out.println(f);
        }
    }
}

2.6 递归遍历目录

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class HelloJava {
    public static void main(String[] args) throws Exception {
        List<String> files = new ArrayList<>();
        DirectoryRecursive.listFiles(files, "/Users/yzy/tmpy/python");
        
        for (String filePath : files) {
            File file = new File(filePath);
            System.out.println(filePath);
            System.out.println(file.getName());
        }
    }
}


class DirectoryRecursive {
    public static void listFiles(List<String> files, String filePath) {
        File[] currentFiles = new File(filePath).listFiles();
        if (currentFiles == null || currentFiles.length == 0) {
            return;
        }
        for (File file : currentFiles) {
            if (file.isFile()) {
                files.add(file.getAbsolutePath());
            } else if (file.isDirectory()) {
                listFiles(files, file.getAbsolutePath());
            }
        }
    }
}

2.7 路径操作

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;

public class HelloJava {
    public static void main(String[] args) throws Exception {
        // 拼接路径
        Path p = Paths.get("/etc/", "ssh", "sshd_config");
        String s = p.toAbsolutePath().toString();
        System.out.println(s);
        
        // 转换成File对象
        File f = p.toRealPath().toFile();
        System.out.println(f);
    }
}

三. InputStream

3.1 读取字节流

  • InputStream是一个抽象类。
  • 抽象方法read签名如下,表示读取输入流的下一个字节,并返回字节表示的int值(0~255,一个中文字符返回3个数字)。如果已读到末尾,返回-1表示不能继续读取了。InputStream和OutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源。
public abstract int read() throws IOException;
  • FileInputStream是InputStream的一个子类,从文件流中读取数据。
public static void readFile() throws IOException {
    InputStream input = new FileInputStream("/etc/profile");
    while (true) {
        int n = input.read();
        if (n == -1) {
            break;
        }
        System.out.println(n);
    }
    input.close();
}
  • 为了保证在发生错误的时候的时候资源能正确的关闭,使用异常捕获
public static void readFile() throws IOException {
    try (InputStream input = new FileInputStream("/etc/profile")) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println(n);
        }
    } // 编译器在此自动为我们写入finally并调用close()
}
  • 编译器只看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法

3.2 缓冲批量读

对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
public static void readFile() throws IOException {
    try (InputStream input = new FileInputStream("/etc/profile")) {
        byte[] buffer = new byte[100];
        int n;
        while ((n = input.read(buffer)) != -1) {
            System.out.println("read " + n + " bytes: " + Arrays.toString(buffer));
        }
    }
}

3.3 实现类FileInputStream

  • 读取文件

3.4 实现类ByteArrayInputStream

  • ByteArrayInputStream可以在内存中模拟一个InputStream
public static void readFile() throws IOException {
    byte[] data = { 72, 101, 108, 108, 111, 33 };
    try (InputStream input = new ByteArrayInputStream(data)) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println((char)n);
        }
    }
}

3.5 实现类ServletInputStream

  • 从HTTP请求读取数据,是最终数据源;

3.6 实现类Socket.getInputStream()

  • 从TCP连接读取数据,是最终数据源;

3.7 读取字符串

  • 只能读取纯文本的文件
public class Main{
    public static void main(String[] args) throws Exception {
        System.out.println(readAsString());
    }
    public static String readAsString() throws IOException {
        try (InputStream input = new FileInputStream("/etc/profile")) {
            int n;
            StringBuilder sb = new StringBuilder();
            while ((n = input.read()) != -1) {
                sb.append((char)n);
            }
            return sb.toString();
        }
    }
}

四. OutputStream

OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:

public abstract void write(int b) throws IOException;
public static void writeFile() throws IOException {
    try (OutputStream output = new FileOutputStream("/tmp/hello.txt")) {
        output.write(72); // H
        output.write(101); // e
        output.write(108); // l
        output.write(108); // l
        output.write(111); // o
    }
    
    try (OutputStream output = new FileOutputStream("/tmp/hello2.txt")) {
        output.write("Hello".getBytes(StandardCharsets.UTF_8));
    }
}

4.1 实现类FileOutputStream

4.2 实现类ByteArrayOutputStream

  • ByteArrayOutputStream可以在内存中模拟一个OutputStream
public static void writeFile() throws IOException {
    byte[] data;
    try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        output.write("Hello".getBytes(StandardCharsets.UTF_8));
        data = output.toByteArray();
    }
    System.out.println(new String(data, StandardCharsets.UTF_8));
}

五. 操作多个文件

  • 快速复制文件
public static void writeFile() throws IOException {
    // 读取input.txt,写入output.txt
    try (InputStream input = new FileInputStream("/tmp/input.txt");
         OutputStream output = new FileOutputStream("/tmp/output.txt")) {
        input.transferTo(output);
    }
}

六. 自定义数据流

  • 自定义Stream主要由:FilterInputStream 和 FilterOutStream 实现
  • 自定义InputStream或者OutputStream可以自定义更多的功能。下面是一个获取字节数的 InputStream。
import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] data = "hello, world!".getBytes(StandardCharsets.UTF_8);
        try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println((char)n);
            }
            System.out.println("Total read " + input.getBytesRead() + " bytes");
        }
    }
}

class CountInputStream extends FilterInputStream {
    private int count = 0;
    
    CountInputStream(InputStream in) {
        super(in);
    }
    
    public int getBytesRead() {
        return this.count;
    }
    
    public int read() throws IOException {
        int n = in.read();
        if (n != -1) {
            this.count ++;
        }
        return n;
    }
    
    public int read(byte[] b, int off, int len) throws IOException {
        int n = in.read(b, off, len);
        this.count += n;
        return n;
    }
}

七. 序列化与反序列化

  • 序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
  • 一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口。Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
  • Java的序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;

public class Main {
    public static void main(String[] args) throws Exception {
        OutputStream buffer1 = new FileOutputStream("/tmp/hello1");
        OutputStream buffer2 = new FileOutputStream("/tmp/hello2");
        
        // 序列化
        try (ObjectOutputStream output1 = new ObjectOutputStream(buffer1);
             ObjectOutputStream output2 = new ObjectOutputStream(buffer2)) {
            // 写入基本类型
            output1.writeInt(12345);
            
            // 写入实现了Serializable接口的Object
            output2.writeObject(new Person(9));
        }
        
        // 反序列化
        try (ObjectInputStream input1 = new ObjectInputStream(new FileInputStream("/tmp/hello1"));
             ObjectInputStream input2 = new ObjectInputStream(new FileInputStream("/tmp/hello2"))) {
            int n = input1.readInt();
            Person p = (Person) input2.readObject();
            System.out.println("n = " + n);
            System.out.println("p.getAge() = " + p.getAge());
        }
    }
}

class Person implements Serializable {
    private int age;
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    public Person(int age) {
        this.age = age;
    }
}

八. Reader与Writer

8.1 Reader

  • Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取
  • java.io.Reader是所有字符输入流的超类,它最主要的方法是:
public int read() throws IOException;
  • 这个方法读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1。

8.2 FileReader

// 单个字符读取
try (Reader reader = new FileReader("/etc/profile", StandardCharsets.UTF_8);) {
    int n;
    while ((n = reader.read()) != -1) {
        System.out.println((char) n);
    }
}

// 批量字符读取
try (Reader reader = new FileReader("/etc/hosts", StandardCharsets.UTF_8);) {
    char[] buffer = new char[1000];
    int n;
    while ((n = reader.read(buffer)) != -1) {
        System.out.println("read " + n + " chars.");
    }
}

8.3 CharArrayReader

  • 它的作用实际上是把一个char[]数组变成一个Reader,
try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}

8.4 StringReader

  • StringReader可以直接把String作为数据源,它和CharArrayReader几乎一样
try (Reader reader = new StringReader("Hello")) {
}

8.5 InputStreamReader

  • 如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。
// 持有InputStream:
InputStream input = new FileInputStream("/etc/profile");

// 变换为Reader:
Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8);

// 简写
try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), StandardCharsets.UTF_8)) {
    // TODO
}

8.6 Writer

  • Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
  • Writer是所有字符输出流的超类,它提供的方法主要有:
  • 写入一个字符(0~65535):void write(int c);
  • 写入字符数组的所有字符:void write(char[] c);
  • 写入String表示的所有字符:void write(String s);

8.7 FileWriter

try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
    writer.write('H');                   // 写入单个字符
    writer.write("Hello".toCharArray()); // 写入char[]
    writer.write("Hello");               // 写入String
}

8.9 CharArrayWriter

try (CharArrayWriter writer = new CharArrayWriter()) {
    writer.write(65);
    writer.write(66);
    writer.write(67);
    char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}

8.10 StringWriter

try (Writer writer = new StringWriter()) {
    writer.write("Hello World!");
    System.out.println(writer.toString());
}

8.11 OutputStreamWriter

  • OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器。
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), StandardCharsets.UTF_8)) {
    // TODO
}

九. 最佳实践

从Java 7开始,提供了Files和Paths这两个工具类,能极大地方便我们读写文件。

9.1 读取为byte[]

byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));

9.2 读取为字符串

// java11
String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.UTF_8);

// java8
String content = new String(Files.readAllBytes(Paths.get("/path/to/file.txt")));

// 上面的 java8 可能读取中文乱码,指定utf8格式读取可以避免
List<String> lines = Files.readAllLines(Paths.get(filePath), StandardCharsets.UTF_8);
Strings.join("\n", lines)

9.3 按行读取返回List

List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));

9.4 一次读取一行

try (BufferedReader br = new BufferedReader(new FileReader("/etc/profile"));) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

9.5 写入二进制文件

Files.write(Paths.get("/tmp/file.txt"), "data".getBytes());

9.6 写入字符串

Files.write(Paths.get("/tmp/file.txt"), "data");

9.7 按行写入数组

字符串列表按行写入。

List<String> lines = new ArrayList<>();
lines.add("line one");
lines.add("line two");
Files.write(Paths.get("/tmp/file.txt"), lines);

9.8 递归删除目录

/**
* 递归删除目录
*/
public static boolean rm_rf(String path) {
    File f = new File(path);
    if (f.isDirectory()) {
        File[] files = f.listFiles();
        if (files != null) {
            for (File file : files) {
                rm_rf(file.toString());
            }
        }
    }
    return f.delete();
}

9.9 注意

Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。

🔗

文章推荐