一文掌握Java IO流:原理、分类与实战

4,535 阅读17分钟

I/O 流

IO 流:存储数据和读取数据的解决方案,input / output流就是像水一样的传输数据

IO流按照操作文件的类型可为

  1. 字节流:可以操作所有类型的文件
  2. 字符流:只能操作纯文本文件

IO 流的体系图

image.png


缓冲流体系:缓冲流就是会增加一个缓冲区,提高文件读写的效率,字符缓冲流提升的不是很明显(因为字符流本身会创建一个8192字节大小的缓冲区)


IO 流原则

  1. 随用随创建(不要提前创建,可能会覆盖文件)
  2. 什么时候不用什么时候关闭

字节流

FileInputStream:操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来

使用步骤

  1. 创建字节输入流对象(FileInputStream)
    • public FileInputStream(File file, true/false)
    • public FileInputStream(String pathname, true/false)
    • 如果文件不存在,就直接报错;如果写入参数 true 那么就会在原有文件内继续添加
  2. 读数据
    • public int read()
      • 一次读一个字节数据,读出来的是数据在 ASCII 上对应的数字
      • 读取一个数据就移动一次指针,读到文件末尾了,read 方法返回 -1
    • public int read(byte[] buffer)
      • 一次读取多个字节数据,具体读多少,跟数组的长度有关
      • 返回值:本次读取到了多少个字节数据
      • 使用public String(char value[], int offset, int count)转换为字符串
      • 读取结束的时候,read 方法会方法一个 -1
  3. 释放资源
// 1. 创建输入流对象,指定文件路径
FileInputStream fis = new FileInputStream("example.txt");
// 2. 逐字节读取文件内容
int data;
while ((data = fis.read()) != -1) { // read() 返回单个字节,读到末尾时返回 -1
    System.out.print((char) data);  // 强转为 char,输出字符
// 3. 关闭流,释放系统资源
fis.close();

FileOutputStream:操作本地文件的字节输出流,可以把程序中的数据写到本地文件中,可以把程序中的数据写到本地文件上,是字节流的基本流

使用步骤

  1. 创建字节输出流对象(FileOutputStream)
    • public FileOutputStream( File file, true/false)
    • public FileOutputStream(String pathname, true/false)
    • 如果文件不存在,就直接报错;如果写入参数 true 那么就会在原有文件内继续添加;如果文件不存在会创建一个新的文件,但是要保证父级路径是存在的
  2. 写出数据
    • public void write(int b)
      • 一次写一个字节数据
      • write方法的参数是整数,但是实际上写到本地文件中的是整数在ASCII上对应的字符
    • void write(byte[] b)
      • 一次写一个字节数组数据
    • void write(byte[] b, int off, int len)
      • 一次写一个字节数组的部分数据
      • 参数一是字节数组;参数二是起始索引;参数三是个数
  3. 释放资源

字符流

字符流的底层就是字节流。字符流 = 字节流 + 字符集

FileReader:操作本地文件的字符输入流,可以把本地文件中的数据读取到程序中来

  1. 创建对象,创建字符输入流关联本地文件
    • public FileReader(File file, true/false)
    • public FileReader(String pathname, true/false)
    • 如果文件不存在,就直接报错
  2. 读取数据
    • public int read()
      • 读取数据,读到末尾返回-1
    • public int read(char[] buffer)
      • 读取多个数据,读到末尾返回-1
      • 读取数据,解码,强制转换三个步骤合并了,把强转之后的字符放到数组当中 等同于空参的read + 强转类型转换
      • 使用public String(char value[], int offset, int count)转换为字符串
  3. 释放资源
    • public void close()
      • 释放资源/关流

FIleWriter:操作本地文件的字节输出流,可以把程序中的数据写到本地文件中

  1. 创建对象,创建字符输出流关联本地文件
    • public FIleWriter(File file, true/false)
    • public FIleWriter(String pathname, true/false)
    • 如果文件不存在,就直接报错
  2. 读取数据
    • void write(int c)
      • 写出一个字符
    • void write(String str)
      • 写出一个字符串
    • void write(String str, int off, int len)
      • 写出一个字符串的一部分
    • void write(char[ ] cbuf)
      • 写出一个字符数组
    • void write(char[ ] cbuf, int off, int len)
      • 写出字符数组的一部分
  3. 释放资源
    • public void close()
      • 释放资源/关流

字符流的原理

  1. 字符输入流原理
    • 创建字符输入流对象
      • 底层:关联文件,并创建缓冲区(长度为 8192 的字节数组)
    • 读取数据
      • 判断缓冲区中是否有数据可以读取
      • 缓冲区没有数据:就从文件中获取数据,装到缓冲区中,每次尽可能装满缓冲区,如果文件中也没有数据了,返回-1
      • 缓冲区有数据:就从缓冲区中读取。(空参的read方法:一次读取一个字节,遇到中文一次读多个字节,把字节解码并转成十进制返回,有参的read方法:把读取字节,解码,强转三步合并了,强转之后的字符放到数组中)
  2. 字符输出流原理
    • 字符流输出和字符流输入都有一个 8192 字节的缓冲区,当缓冲区满了就会自动将数据写入目的地
    • public void flush()
      • 刷新之后,还可以继续往文件中写出数据
    • public void close()
      • 断开通道,无法再往文件中写出数据

字符集

在计算机中,任意数据都是以二进制的形式来存储的;计算机中最小的存储单元是一个字节;简体中文版 Windows,默认使用 GBK 字符集;GBK 字符集完全兼容 ASCII 字符集

ASCII 字符集中,一个英文占一个字节。一个英文占一个字节,二进制第一位是 0

汉字两个字节存储,二进制高位字节的第一位是 1,转成十进制之后是一个负数

Unicode,UTF(Unicode Transfer Format)。Unicode 字符集的 UTF-8 编码格式

  • 一个英文占一个字节,二进制第一位是 0,转成十进制是正数
  • 一个中文占三个字节,二进制第一位是 1,第一个字节转成十进制是负数

  1. Java 的编码的方法
    • public byte[] getBytes()使用默认方式进行编码
    • public byte[] getBytes(String charsetName)使用指定方式进行编码
  2. Java 中解码的方法
    • String(byte[] bytes)使用默认方式进行解码
    • String(byte[] bytes, String charsetName)使用指定方式进行解码

缓冲流

**字节缓冲流:**缓冲流提高效率的原理,就是在内存中创建一个缓存区,减少了磁盘的读写次数,中间的遍历只是为了在输入输出缓冲流之间进行“倒手数据”(在内存中这个速度非常快)

  • 创建一个 size 字节大小的字节缓存输入流(把基本流包装为高级流)
    • public BufferedInputStream(InputStream in, int size)
  • 创建一个 size 字节大小的字节缓存输出流(把基本流包装为高级流)
    • public BufferedOutputStream(OutputStream out, int size)
  • 读写入一个字节的数据
    • read() // write(int c)
  • 读写入多个字节的数据
    • read(byte[] bytes) // write(bytes, 0, len)

字符缓冲流

创建一个size*2字节大小的字符缓存输入流(因为 char 类型在 Java 中的大小是两字节) BufferedReader 把基本流包装为高级流public BufferedReader(Reader r)

特有方法,读一整行:public String readLine()

  • 该方法不会将 换行符 读入到缓冲区中
  • 读到结尾的时候,该方法返回 null

创建一个size*2字节大小的字符缓存输出流(因为 char 类型在 Java 中的大小是两字节) BufferedWriter 把基本流包装为高级流public BufferedWriter(Writer r)

特有方法,跨平台的换行public void newLine()

  • 会根据不同的操作系统写入一个换行符

转换流

字符转换输入流:InputstreamReader

字符转换输出流:0utputStreamWriter

转化流是字节流和字符流之间的桥梁。字节流在读取中文的时候,是会出现乱码的,但是字符流可以搞定

//1.字节流在读取中文的时候,是会出现乱码的,但是字符流可以搞定
FileInputStream fis = new FileInputStream("gbk.txt");
// 包装字节流为转换流,这就就能按照字节读取且不乱码
InputStreamReader isr = new InputStreamReader(fis, Charset.forName("GBK"));
//2.字节流里面是没有读一整行的方法的,只有字符缓冲流才能搞定
BufferedReader br = new BufferedReader(isr); // 只有 BufferedReader 缓冲流 才能按行读取,所以需要将转换流继续包装为缓冲流
String line;
while ((line = br.readLine()) != null)
    System.out.println(line);
br.close();

序列化流 / 反序列化流

序列化流的对象 / 对象操作输出流

  1. 把基本流变成高级流public ObjectOutputStream (OutputStream out)
  2. 把对象序列化(写出)到文件中去public final void writeObject (Object obj)
    • 对象必须要实现 Serializable 接口 (这个接口只是一个标记性接口,里面没有抽象方法,只表示当前的类可以被序列化), 如果没有实现接口,就会抛出 NotSerializableException 异常
  3. 释放资源public void close()
// 1.创建对象
Student stu = new Student("zhangsan",23);
// 2.创建序列化流的对象/对象操作输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student_objet.txt"));
// 3.写出数据
oos.writeObject (stu);
// 4.释放资源
oos.close();

反序列化流 / 对象操作输入流

  1. 把基本流变成高级流public ObjectInputStream(InputStream out)
  2. 把文件反序列化(读入)到程序中去public Object readObject()
    • 对象必须要实现 Serializable 接口 (这个接口只是一个标记性接口,里面没有抽象方法,只表示当前的类可以被反序列化), 如果没有实现接口,就会抛出 deserialization 异常
  3. 释放资源public void close()
// 1.创建反序列化流的对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student_objet.txt"));
// 2.读取数据
Student stu = (Student)ois.readObject();
// 3.打印对象
System.out.println(stu);
// 4.释放资源
ois.close();

注意:

  1. 使用序列化流将对象写到文件时,需要让 Javabean 类实现 Serializable 接口。否则,会出现 NotSerializableException 异常
  2. 序列化流写到文件中的数据是不能修改的,一旦修改就无法再次读回来了
  3. 序列化对象后,修改了Javabean类,再次反序列化,会不会有问题?
    • 会出问题,会抛出InvalidclassException异常
    • 解决方案:给 Javabean 类添加 serialVersionUID (列号、版本号)
      • 方法1:手动写private static final long serialVersionUID = num;
      • 方法2:IDEA中修改 Serializable
      • 方法3:从别的类中直接复制
  4. 如果一个对象中的某个成员变量的值不想被序列化,又该如何实现呢?
    • 解决方案:给该成员变量加 transient(瞬态关键字)关键字修饰,该关键字标记的成员变量不参与列化过程
  5. 当需要序列化多个对象的时候,通常的做法是将对象添加到一个集合中,再将集合序列化到文件中。这样在反序列化的时候,就不需要考虑有多少个对象了

打印流

打印流只能写不能读,即打印流不操作数据源,只能操作目的地

字节打印流

  • 关联字节输出流/文件/文件路径
    • public PrintStream(OutputStream/File/String)
  • 指定字符编码
    • public PrintStream(String fileName, Charset charset)
  • 自动刷新
    • public PrintStream(OutputStreamout, boolean autoFlush)
  • 指定字符编码且自动刷新
    • public PrintStream(OutputStream out, boolean autoFlush, String encoding)
  • 将指定的字节写出
    • public void write(int b)
  • 特有方法:打印任意数据,自动刷新,自动换行
    • public void println(Xxx xx)
  • 特有方法:打印任意数据,不换行
    • public void print(Xxx xx)
  • 特有方法:带有占位符的打印语句,不换行
    • public void printf(String format, Object... args)
package myprintstream;

import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.util.Date;

public class Demo2 {
    public static void main(String[] args) throws FileNotFoundException {
        PrintStream ps = new PrintStream("day27-code\\src\\myprintstream\\占位符.txt");

        //% n表示换行
        ps.printf("我叫%s %n", "阿玮");
        ps.printf("%s喜欢%s %n", "阿珍", "阿强");
        ps.printf("字母H的大写:%c %n", 'H');
        ps.printf("8>3的结果是:%b %n", 8 > 3);
        ps.printf("100的一半是:%d %n", 100 / 2);
        ps.printf("100的16进制数是:%x %n", 100);
        ps.printf("100的8进制数是:%o %n", 100);
        ps.printf("50元的书打8.5折扣是:%f元%n", 50 * 0.85);
        ps.printf("计算的结果转16进制:%a %n", 50 * 0.85);
        ps.printf("计算的结果转科学计数法表示:%e %n", 50 * 0.85);
        ps.printf("计算的结果转成指数和浮点数,结果的长度较短的是:%g %n", 50 * 0.85);
        ps.printf("带有百分号的符号表示法,以百分之85为例:%d%% %n", 85);
        ps.println("---------------------");

        double num1 = 1.0;
        ps.printf("num: %.4g %n", num1);
        ps.printf("num: %.5g %n", num1);
        ps.printf("num: %.6g %n", num1);

        float num2 = 1.0F;
        ps.printf("num: %.4f %n", num2);
        ps.printf("num: %.5f %n", num2);
        ps.printf("num: %.6f %n", num2);
        ps.println("---------------------");

        ps.printf("数字前面带有0的表示方式:%03d %n", 7);
        ps.printf("数字前面带有0的表示方式:%04d %n", 7);
        ps.printf("数字前面带有空格的表示方式:% 8d %n", 7);
        ps.printf("整数分组的效果是:%,d %n", 9989997);
        ps.println("---------------------");

        //最终结果是10位,小数点后面是5位,不够在前面补空格,补满10位
        //如果实际数字小数点后面过长,但是只规定两位,会四舍五入
        //如果整数部分过长,超出规定的总长度,会以实际为准
        ps.printf("一本书的价格是:%2.5f元%n", 49.8);
        ps.printf("%(f%n", -76.04);

        //%f,默认小数点后面7位,
        //<,表示采取跟前面一样的内容
        ps.printf("%f和%3.2f %n", 86.04, 1.789651);
        ps.printf("%f和%<3.2f %n", 86.04, 1.789651);
        ps.println("---------------------");

        Date date = new Date();
        // %t 表示时间,但是不能单独出现,要指定时间的格式
        // %tc 周二 12月 06 22:08:40 CST 2022
        // %tD 斜线隔开
        // %tF 冒号隔开(12小时制)
        // %tr 冒号隔开(24小时制)
        // %tT 冒号隔开(24小时制,带时分秒)
        ps.printf("全部日期和时间信息:%tc %n", date);
        ps.printf("月/日/年格式:%tD %n", date);
        ps.printf("年-月-日格式:%tF %n", date);
        ps.printf("HH:MM:SS PM格式(12时制):%tr %n", date);
        ps.printf("HH:MM格式(24时制):%tR %n", date);
        ps.printf("HH:MM:SS格式(24时制):%tT %n", date);

        System.out.println("---------------------");
        ps.printf("星期的简称:%ta %n", date);
        ps.printf("星期的全称:%tA %n", date);
        ps.printf("英文月份简称:%tb %n", date);
        ps.printf("英文月份全称:%tB %n", date);
        ps.printf("年的前两位数字(不足两位前面补0):%tC %n", date);
        ps.printf("年的后两位数字(不足两位前面补0):%ty %n", date);
        ps.printf("一年中的第几天:%tj %n", date);
        ps.printf("两位数字的月份(不足两位前面补0):%tm %n", date);
        ps.printf("两位数字的日(不足两位前面补0):%td %n", date);
        ps.printf("月份的日(前面不补0):%te  %n", date);

        System.out.println("---------------------");
        ps.printf("两位数字24时制的小时(不足2位前面补0):%tH %n", date);
        ps.printf("两位数字12时制的小时(不足2位前面补0):%tI %n", date);
        ps.printf("两位数字24时制的小时(前面不补0):%tk %n", date);
        ps.printf("两位数字12时制的小时(前面不补0):%tl %n", date);
        ps.printf("两位数字的分钟(不足2位前面补0):%tM %n", date);
        ps.printf("两位数字的秒(不足2位前面补0):%tS %n", date);
        ps.printf("三位数字的毫秒(不足3位前面补0):%tL %n", date);
        ps.printf("九位数字的毫秒数(不足9位前面补0):%tN %n", date);
        ps.printf("小写字母的上午或下午标记(英):%tp %n", date);
        ps.printf("小写字母的上午或下午标记(中):%tp %n", date);
        ps.printf("相对于GMT的偏移量:%tz %n", date);
        ps.printf("时区缩写字符串:%tZ%n", date);
        ps.printf("1970-1-1 00:00:00 到现在所经过的秒数:%ts %n", date);
        ps.printf("1970-1-1 00:00:00 到现在所经过的毫秒数:%tQ %n", date);

        ps.close();
    }
}

字符打印流:字符流底层有缓冲区,想要自动刷新需要开启

  • 关联字节输出流/文件/文件路径
    • public PrintWriter(Write/File/String)
  • 指定字符编码
    • public PrintWriter(String fileName, Charset charset)
  • 自动刷新
    • public PrintWriter(Write, boolean autoFlush)
  • 指定字符编码且自动刷新
    • public PrintWriter(Write out, boolean autoFlush, String encoding)
  • 常规方法:规则跟之前一样,将指定的字节写出
    • public void write(int b)
  • 特有方法:打印任意数据,自动刷新,自动换行
    • public void println(Xxx xx)
  • 特有方法:打印任意数据,不换行
    • public void print(Xxx xx)
  • 特有方法:带有占位符的打印语句,不换行
    • public void printf(String format, Object... args)

打印流的一个应用

  • 获取打印流的对象,此打印流在虚拟机启动的时候,由虚拟机创建,默认指向控制台
  • 特殊的打印流,系统中的标准输出流。是不能关闭,在系统中是唯一的
PrintStream ps = System.out;
ps.println("123");
ps.close();
ps.println("你好你好");
System.out.println("456");

压缩流

解压缩流:解压的本质就是把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中

public static void unzip(File src,File dest) throws IOException {
    // 解压的本质:把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中
    // 创建一个解压缩流用来读取压缩包中的数据
    ZipInputStream zip = new ZipInputStream(new FileInputStream(src));
    // 要先获取到压缩包里面的每一个zipentry对象
    // 表示当前在压缩包中获取到的文件或者文件夹
    ZipEntry entry;
    while((entry = zip.getNextEntry()) != null){
        System.out.println(entry);
        if(entry.isDirectory()){
            //文件夹:需要在目的地dest处创建一个同样的文件夹
            File file = new File(dest,entry.toString());
            file.mkdirs();
        }else{
            //文件:需要读取到压缩包中的文件,并把他存放到目的地dest文件夹中(按照层级目录进行存放)
            FileOutputStream fos = new FileOutputStream(new File(dest,entry.toString()));
            int b;
            while((b = zip.read()) != -1){
                //写到目的地
                fos.write(b);
            }
            fos.close();
            //表示在压缩包中的一个文件处理完毕了。
            zip.closeEntry();
        }
    }
    zip.close();
}

压缩流(单个文件),创建一个 zip 文件,利用 ZipEntry 对象将文件写入到 zip 文件中

public static void myZip(File src, File dest) throws IOException {
    // 1.创建一个 zip 文件
    ZipOutputStream zipos = new ZipOutputStream(new FileOutputStream(new File(dest, "a.zip")));
    // 2.创建文件或文件夹的 entry 对象,表示压缩包中的文件和文件夹
    // 参数:压缩包里面的路径 (这个参数很重要)
    ZipEntry entry = new ZipEntry("a.txt");
    // 3.将 entry 对象写入到压缩包中
    zipos.putNextEntry(entry);
    // 读取需要压缩文件中的内容,写入到entry中去
    FileInputStream fos = new FileInputStream(src);
    // 4.像entry中写入内容
    int b;
    while ( (b=fos.read()) != -1 )
        zipos.write(b);
    // 关闭 entry 对象,表示一个文件处理完毕
    zipos.closeEntry();
    zipos.close();
}

压缩流(文件夹),利用源文件的父级路径创建目标路径,然后利用压缩流关联压缩包,利用 ZipEntry 对象向压缩包中写入数据。利用递归处理文件夹

/*
 *   压缩流
 *      需求:
 *          把D:\\aaa文件夹压缩成一个压缩包
 * */
//1.创建File对象表示要压缩的文件夹
File src = new File("day27-code\\src\\myzipstream\\aaa");
//2.创建File对象表示压缩包放在哪里(压缩包的父级路径)
File destParent = src.getParentFile();
//3.创建File对象表示压缩包的路径
File dest = new File(destParent, src.getName() + ".zip");
//4.创建压缩流关联压缩包
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
//5.获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
toZip(src, zos, src.getName());//aaa
//6.释放资源
zos.close();


/*
 *   作用:获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
 *   参数一:数据源
 *   参数二:压缩流
 *   参数三:压缩包内部的路径
 * */
public static void toZip(File src, ZipOutputStream zos, String name) throws IOException {
    //1.进入src文件夹
    File[] files = src.listFiles();
    //2.遍历数组
    for (File file : files) {
        if (file.isFile()) {
            //3.判断-文件,变成ZipEntry对象,放入到压缩包当中(一定要写上压缩包中的路径)
            ZipEntry entry = new ZipEntry(name + "\\" + file.getName());//aaa\\no1\\a.txt
            zos.putNextEntry(entry);
            //读取文件中的数据,写到压缩包
            FileInputStream fis = new FileInputStream(file);
            int b;
            while ((b = fis.read()) != -1) {
                zos.write(b);
            }
            fis.close();
            zos.closeEntry();
        } else {
            //4.判断-文件夹,递归
            toZip(file, zos, name + "\\" + file.getName());
            //     no1            aaa   \\   no1
        }
    }
}