跳至主要內容

Java I/O流基础

Zenghr大约 10 分钟Java

I/O 流基础知识

提示

本文旨意掌握 I/O 流的基础知识以及流的基本操作。

File 类

File 理解为文件和文件夹(目录),用于表示磁盘中某个文件或文件夹的路径。该类包含了文件的创建、删除、重命名、判断是否存在等方法。只能设置和获取文件本身的信息(文件大小,是否可读),不能设置和获取文件里面的内容。

  • Unix: 严格区分大小写,使用 (/) 来表示路径分隔符。
  • Windows: 默认情况下是不区分大小写的,使用 (\) 来分割目录路径。但是在Java中一个 (\) 表示转义,所以在 Windows 系统中就得使用两个 \\

操作 File 的常用方法

  • getName() - 获取文件名
  • getParent() - 获取上层路径名称(如果使用的是相对路径没有上层路径,返回 null)
  • getParentFile() - 获取上层路径的 File 对象
  • getAbsolutePath() - 获取绝对路径
  • getAbsoluteFile() - 获取绝对路径的 File 对象
  • list() - 获取该目录下的所有文件名
  • listFiles() - 获取该目录下的所有文件 File 对象
  • exists() - 判断文件是否存在
  • createNewFile() - 创建新文件
  • isDirectory() - 判断是否是目录
  • mkdir() - 创建新目录
  • mkdirs() - 创建新目录(补充不存在的上层目录)
  • renameTo() - 移动重命名文件
  • delete() - 删除文件

字符编码

常见的字符集

  • ASCII:占一个字节,只能包含128个符号。不能表示汉字。
  • ISO-8859-1:也称之为latin-1,占一个字节,收录西欧语言,不能表示汉字。
  • GB2312/GBK/GB18030:占两个字节,支持中文。
  • ANSI:占两个字节,在简体中文的操作系统中ANSI 就指的是 GBK。
  • UTF-8:是一种针对Unicode的可变长度字符编码,是Unicode的实现方式之一,支持中文。在开
    发中建议使用

编码解码操作

数据在网络上传输是以二进制的格式,二进制格式就是 byte 数组,此时需要把信息做编码和解码处理

  • 编码:把字符串转换为byte数组 String--->byte[]
  • 解码:把 byte 数组转换为字符串 byte[]--->String

注意:一定要保证编码和解码的字符集相同,才能正确解码出信息

提示

乱码问题的根本原因就是 编码和解码使用的字符集不是同一种,解决方法 统一编码字符集

四大基流

提示

InputStream/OutputStream 只定义了流的流向和流通道的数据单元,并没有定义源数据源和目的地

java.io包中的类是按照 源数据源 和 目的地 进行划分的。

java.io包中的类命名规则:数据源/目的地 + 数据传输单元 e.g:File + InputStream / File + Reader

根据流的不同特性,流的划分是不一样的,一般按照如下情况来考虑:

  • 按流动方向:分为 输入流 和 输出流
  • 按数据传输单位:分为 字节流 和 字符流,即每次传递一个字节(byte)或一个字符(char)
  • 按功能上划分:分为 节点流 和 处理流,节点流功能单一,处理流功能更强
流向字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

字节输入/字节输出流

mark
mark

InputStream(字节输入流)

InputStream 代表字节输入流的所有类的父类

常用方法

方法方法作用
int read()从输入流中读取一个字节数据并返回该字节数据,如果到达流的末尾,则返回 -1
int read(byte[] buff)从输入流中读取多个字节数据,并存储在缓冲区数组 buff 中。返回已读取的字节数量,如果已到达流的末尾,则返回 -1
void close()关闭此输入流并释放与该流关联的所有系统资源。InputStream 的 close 方法不执行任何操作

OutputStream(字节输出流)

OutputStream 代表字节输出流的所有类的父类

常用方法

方法方法作用
void write(int b)将指定的一个字节数据b写入到输出流中
void write(byte[] buff)把数组buff中所有字节数据写入到输出流中
void write(byte[] buff, int off, int len)把数组buff中从索引 off 开始的 len 个字节写入此输出流中
void flush()刷新缓冲区
void close()关闭此输出流并释放与此流有关的所有系统资源

文件字节流

根据 IO 包中类的命名规则,当程序操作的源数据是文件时,输入字节流使用 FileInputStream 实现类,输出字节流使用 FileOutputStream 实现类

public static void main(String[] args) {
    // 源数据
    File srcFile = new File("E:\\test\\test.txt");
    File decFile = new File("test.txt");

    // todo: jdk 1.7 语法,实现了 AutoCloseable 接口会自动关闭
    try (
        FileInputStream in = new FileInputStream(srcFile);
        FileOutputStream out = new FileOutputStream(decFile);
    ) {
        // 循环读取
        int len;
        byte[] buf = new byte[1024];
        while ((len = in.read(buf)) != -1) {
            out.write(buf, 0, len);
            out.flush();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }


}

字符输入/输出流

mark
mark

Reader(字符输入流)

Reader 表示字符输入流的所有类的超类

常用方法

方法方法作用
int read()从输入流中读取一个字节数据并返回该字节数据,如果到达流的末尾,则返回 -1
int read(char[] cbuf)从输入流中读取多个字节数据,并存储在缓冲区数组 cbuf 中。返回已读取的字节数量,如果已到达流的末尾,则返回 -1
void close()关闭此输入流并释放与该流关联的所有系统资源

Writer(字符输出流)

Writer 代表字符输出流的所有类的超类

常用方法

方法方法作用
void write(int c)将指定的一个字符数据 c 写入到输出流中
void write(char[] cbuf)把数组 cbuf 中 cbuf.length 个字符数据写入到输出流中
void write(char[] cbuf, int off,int len)把数组 cbuf 中从索引 off 开始的 len 个字符写入此输出流中
void write(String str)将str字符串数据写入到输出流中
void close()关闭此输入流并释放与该流关联的所有系统资源
void flush()刷新此输出流并强制写出所有缓冲的输出字符

文件字符流

  • FileReader - 操作文件的字符输入流
  • FileWriter - 操作文件的字符输出流
public static void main(String[] args) {
    // 源数据
    File srcFile = new File("E:\\test\\test.txt");
    File decFile = new File("test.txt");

    // todo: jdk 1.7 语法,实现了 AutoCloseable 接口会自动关闭
    try (
        FileReader in = new FileReader(srcFile);
        FileWriter out = new FileWriter(decFile);
    ) {
        // 循环读取
        int len;
        char[] buf = new char[1024];
        while ((len = in.read(buf)) != -1) {
            out.write(buf, 0, len);
            out.flush();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

补充知识点

提示

在文件对象中写入内容的时候,如果该文件对象已经有了其他的内容,这时候我们在使用 write 方法写入数据的时候,会覆盖掉原有的内容,如果想要在原有的内容上追加需要使用构造器 FileWriter(String fileName, boolean append) ,设置为 true,添加数据就会在原有内容的基础上追加

在 JDK 1.7 的新语法中,实现了 AutoCloseable 接口的类会自动帮你关闭资源,语法如下👇

try (
    FileInputStream in = new FileInputStream(srcFile);
    FileOutputStream out = new FileOutputStream(decFile);
) {
    // ... 代码块
} catch (Exception e) {
    // ..
}

按资源分类I/O流

mark
mark

缓冲流

节点流的功能都比较单一,性能较低。

处理流,也称之为包装流,相对于节点流更高级,这里存在一个设计模式—— 装饰设计模式

如需要进一步了解 装饰者模式 可以看:Java IO - 设计模式(装饰者模式)

缓冲流内置了一个大小为 8192 的缓冲区,用于减少磁盘的操作,存满 8192 缓冲区才会写入磁盘中,操作数据量比较大的流,建议都用缓冲流,缓冲流根据四大基流都有对应的缓冲流

BufferedInputStream / BufferedOutputStream / BufferedReader / BufferedWriter

字节缓冲流

字节缓冲流的 API 方法基本和 字节基流的操作一致

  • BufferedInputStream
  • BufferedOutputStream
public static void main(String[] args) {
    // 源数据
    File srcFile = new File("E:\\test\\test.txt");
    File decFile = new File("test.txt");

    // todo: jdk 1.7 语法,实现了 AutoCloseable 接口会自动关闭
    try (
        FileInputStream in = new FileInputStream(srcFile);
        FileOutputStream out = new FileOutputStream(decFile);
        // buff 缓冲区包装类
        BufferedInputStream buffIn = new BufferedInputStream(in);
        BufferedOutputStream buffOut = new BufferedOutputStream(out);
    ) {
        // 循环读取
        int len;
        byte[] buf = new byte[1024];
        while ((len = buffIn.read(buf)) != -1) {
            buffOut.write(buf, 0, len);
        }
        buffOut.flush();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

字符缓冲流

BufferedReader 继承于 Reader,实现对文本型文件进行高效(一次读取一行)的读取,提供特有的方法 readLine() 读取一行

BufferedWriter 继承于 Writer,实现对文本型文件进行高效的写入,提供特有的方法 newLine() 写入一个换行

ublic static void main(String[] args) {
    // 源数据
    File srcFile = new File("E:\\test\\test.txt");
    File decFile = new File("test.txt");

    // todo: jdk 1.7 语法,实现了 AutoCloseable 接口会自动关闭
    try (
        FileReader in = new FileReader(srcFile);
        FileWriter out = new FileWriter(decFile);
        // buff 缓冲区包装类
        BufferedReader buffIn = new BufferedReader(in);
        BufferedWriter buffOut = new BufferedWriter(out);
    ) {
        // 循环读取每一行
        String line;
        while ((line = buffIn.readLine()) != null) {
            // 一行一行写入文件
            buffOut.write(line);
            // 写入换行
            buffOut.newLine();
        }
        // 刷新缓冲区
        buffOut.flush();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

serializable(对象序列化)

序列化: 指把 Java 堆内存中的对象数据,通过某种方式把对象数据存储到磁盘文件中或者传递给给网络上传输。序列化在分布式系统在应用非常广泛

反序列化: 把磁盘文件中的对象的数据或者把网络节点上的对象数据恢复成 Java 对象的过程

需要做序列化的对象必须实现序列化接口:java.io.Serializable(这只是标志接口,没有抽象方法)

通过 IO 中的对象流来做序列化和反序列化操作

  • ObjectInputStream: 通过 writeObject 方法操作序列化
  • ObjectOutoutStream: 通过 readObject 方法操作反序列化

注意:如果字段使用 transient 关键字修饰,该字段不会被序列化

// 实现 serializable 接口
public class User implements Serializable {
    private String name;
    private int age;
    // 不序列化
    private transient String password;
	// ... set/get
}

序列化操作

public static void main(String[] args) throws Exception {
    User user = new User("张三", 20, "123456");

    FileOutputStream out = new FileOutputStream("user.dll");
    ObjectOutputStream oos = new ObjectOutputStream(out);
	// 序列化字节写入文件
    oos.writeObject(user);
	// 关闭资源
    oos.flush();
    oos.close();
}

反序列化操作

public static void main(String[] args) throws Exception {
    // 反序列化
    FileInputStream in = new FileInputStream("user.dll");
    ObjectInputStream ois = new ObjectInputStream(in);
	// 读取文件 反序列化成 java 对象
    User user = (User) ois.readObject();
    System.out.println(user);
	// 关闭资源
    ois.close();
}

序列化版本问题

当类实现 Serializable 接口后,在编译的时候就会根据字段生成一个缺省的 serialVersionUID 值,并在序列化操作时,写到序列化数据文件中

但随着项目的升级系统的 class 文件也会升级(增加一个字段/删除一个字段),此时再重新编译,对象的 serialVersionUID 值又会改变。那么在反序列化时,JVM会把对象数据数据中的 serialVersionUID 与本地字节码中的 serialVersionUID 进行比较,如果值不相同(意味着类的版本不同),那么报异常 InvalidClassException,即:类版本不对应,不能进行反序列化。如果版本号相同,则可以进行反序列化,如下👇

mark
mark

为了避免代码版本升级而造成反序列化因版本不兼容而失败的问题,在开发中我们可以故意在类中提供一个固定的 serialVersionUID

class User implements Serializable {
	private static final long serialVersionUID = 1L;
  	// ..
}