Java IO流学习笔记:从字节流到字符流

程序中可以用变量、数组、对象、集合存储数据,但是这些数据都是存储在内存中,电脑断电后数据将不存在。如果要持久化存储,必须将数据保存在硬盘的文件中。Java提供File类对文件和目录进行操作,但是对于文件本身的数据的存储和读取,需要IO流。

IO流中的I是指input输入,O指output输出,IO流是指像水流一样将程序中的数据输入或输出数据到文件中,对文件数据进行操作。

input输入,output输出,是相对于程序而言的,从文件中读取数据到内存,是input输入。程序将数据从内存中存入到文件,是output输出。

从数据流向可以划分为输入流、输出流,从数据类型可以划分为字节流、字符流,两两组合可划分为:输入字节流、输出字节流、输入字符流、输出字符流。对应Java中四种抽象类:

  1. InputStream:输入字节流
  2. OutputStream:输出字节流
  3. Reader:输入字符流
  4. Writer:输出字符流

IO流不仅可以建立程序与文件的数据管道,也可以建立程序与网络存储的数据管道,因此IO流有很多针对不同存储类型的实现类,比如针对文件的实现类为:FileInputStream、FileOutputStream、FileReader、FileWriter。

FileInputStream文件输入字节流

FileInputStream文件输入字节流,顾名思义,是建立与本地文件的,将数据从文件获取到内存的,按照字节进行读取的管道。

FileInputStream构造方法支持通过文件路径字符串(类似File对象构造方法)和通过File对象两种方式创建FileInputStream对象:

java 复制代码
public class App2 {  
    public static void main(String[] args) throws IOException {  
        // 创建文件输入字节流对象  
        // 方式1:通过文件路径字符串创建  
        InputStream is1 = new FileInputStream("test.md");  
  
        // 方式2:通过文件对象创建  
        File file = new File("test.md");  
        InputStream is2 = new FileInputStream(file);  
  
        is1.close();  
        is2.close();  
    }  
}

InputStream抽象类提供三种读取文件数据的方法:

java 复制代码
int read() // 读取一个字节,返回该字节二进制对应的int类型数字。数据为空时,返回-1
int read(byte b[]) // 读取字节数组长度的字节数量的数据,存储到字节数组,并返回这次读取的字节长度。数据为空时,返回-1
int read(byte b[], int off, int len) // 读取len长度字节的数据,存储到字节数组中,要求从off下标开始存储。

调用int read()会读取一个字节,并返回该字节二进制对应的int类型数字。方法调用后,"指针"会往后移,再次调用方法,会读取下一个字节的数据。

java 复制代码
public class App3 {  
    public static void main(String[] args) throws IOException {  
        // 创建文件输入字节流对象  
        InputStream is = new FileInputStream("test.md"); // 文件内存储内容:abc  
  
        // 一个字节一个字节的读取数据  
        int data = is.read(); // 读取一个字节,返回该字节二进制对应的int类型数字  
        System.out.println(data); // 97, 字符a的ASCII码为97,字符集UTF-8兼容ASCII  
        System.out.println(is.read()); // 98  
        
        // 关闭字节流对象  
		is.close();
    }  
}

当文件数据全部读完,read()方法会返回-1,作为结束的标志。

一个字节一个字节的读取数据,效率比较低。因此,一般用int read(byte b[])批量读取数据的方法。

java 复制代码
public class App4 {  
    public static void main(String[] args) throws IOException {  
        // 创建一个文件输入字节流对象  
        InputStream is = new FileInputStream("test.md"); // 文件内存储内容:abcdefghijklmnopqrstuvwxyz  
  
        // 创建一个字节数组,用于存储读取到的数据  
        byte[] buffer = new byte[3]; // 创建一个字节数组,长度为3  
  
        // 循环多次从字节流对象中读取数据,并打印出来  
        while (is.read(buffer) != -1) { // 当数据为空时,返回-1,可以作为循环的结束条件  
            System.out.print(new String(buffer)); // 将字节数组转换成字符串并打印出来  
        }  
  
        // 关闭字节流对象  
        is.close();  
    }  
}

InputStream不常用的字节读取方法int read(byte b[], int off, int len),可读取一定长度的字节数据,按一定偏移量存储到字节数组中。

java 复制代码
public class App5 {  
    public static void main(String[] args) throws IOException {  
        // 创建一个文件输入字节流对象  
        InputStream is = new FileInputStream("test.md"); // 文件内存储内容:abcdefghijklmnopqrstuvwxyz  
  
        // 创建一个字节数组,用于存储读取到的数据  
        byte[] buffer = new byte[6]; // 创建一个字节数组,长度为3  
  
        int num = is.read(buffer, 1,  3); // 读取字节流对象中的数据,并保存在字节数组中  
        System.out.println("读取的字节数为:" + num); // 3 输出读取的字节数  
  
        // 循环打印读取的字节数组,可以通过字节数组的索引值获取字节数据  
        for (int i = 0; i < buffer.length; i++) {  
            System.out.println("字节数组索引" + i + "存储的数据为: " +  buffer[i]);  
        }  
  
        // 关闭字节流对象  
        is.close();  
    }  
}
shell 复制代码
读取的字节数为:3
字节数组索引0存储的数据为: 0
字节数组索引1存储的数据为: 97
字节数组索引2存储的数据为: 98
字节数组索引3存储的数据为: 99
字节数组索引4存储的数据为: 0
字节数组索引5存储的数据为: 0

进程已结束,退出代码为 0

某些时候,我们可能有读取文件所有字节的需求,Java提供一个byte[] readAllBytes()方法,读取输入流中剩余的所有字节:

java 复制代码
public class App6 {  
    public static void main(String[] args) throws IOException {  
        // 创建一个文件输入字节流对象  
        InputStream is = new FileInputStream("test.md"); // 文件内存储内容:abcdefghijklmnopqrstuvwxyz  
  
        // 读取文件所有字节  
        byte[] bytes= is.readAllBytes();  
        System.out.println(new String(bytes)); // abcdefghijklmnopqrstuvwxyz  
  
        // 关闭字节流对象  
        is.close();  
    }  
}

最后的最后,一定别忘了关闭字节流对象,释放系统资源。

FileOutputStream文件输出字节流

FileOutputStream文件输出字节流与FileInputStream文件输入字节流相似,仅方向相反,是建立与本地文件的,从内存中往文件中写入数据,按照字节进行写入的管道。

与FileInputStream相似,FileOutputStream支持通过文件路径字符串和File对象两种方式创建FileOutputStream对象。除此之外,FileInputStream还支持覆盖和追加两种模式。因此,有4种构造方法:

java 复制代码
FileOutputStream(String name) // 通过文件路径字符串创建对象,默认为覆盖模式
FileOutputStream(String name, boolean append) // 通过文件路径字符串创建对象,append形参为true为追加模式
FileOutputStream(File file) // 通过File对象创建字节输出流对象,默认为覆盖模式
FileOutputStream(File file, boolean append) // 通过File对象创建字节输出流对象,append形参为true为追加模式

同InputStream抽象类相似,OutputStream提供三种写入文件数据的方法:

java 复制代码
void write(int b) // 写入一个字节到文件
void write(byte b[]) // 将字节数组中的数据,写入到文件
void write(byte b[], int off, int len) // 将字节数组中从off索引开始,写入长度为len的字节数据到文件

操作示例如下:

java 复制代码
public class App7 {  
    public static void main(String[] args) throws IOException {  
        // 创建文件输出流对象  
        OutputStream os = new FileOutputStream("outputfile.txt"); // 默认为覆盖模式  
  
        // 写入方式1:写入一个字节到文件  
        os.write(65); // 写入A  
  
        // 写入方式2:将字节数组中的数据,写入到文件  
        byte[] bytes = {66, 67, 68, 69};  
        os.write(bytes); // 写入BCDE  
  
        // 写入方式3:将字节数组中从off索引开始,写入长度为len的字节数据到文件  
        os.write(bytes, 1, 2); // 写入CD  
  
        // 关闭文件输出流对象  
        os.close();  
        System.out.println("文件写入成功!");  
        
        /*
        打印上述写入的内容
        */
  
        // 创建文件输入流对象  
        InputStream is = new FileInputStream("outputfile.txt");  
  
        // 打印文件内容  
        byte[] all = is.readAllBytes();  
        for (byte b : all) {  
            System.out.print((char)b);  
        } // ABCDECD  
  
        is.close();  
    }  
}

使用文件输入字节流和文件输出字节流,可是实现复制文件的操作:

java 复制代码
public class App8 {  
    public static void main(String[] args) throws IOException {  
        /*  
        使用文件输入字节流和文件输出字节流,复制一个文件  
         */  
        // 创建一个文件输入字节流对象  
        InputStream is = new FileInputStream("/Users/zhongziqiang/Downloads/33_1.png");  
        // 创建一个文件输出字节流对象  
        FileOutputStream fos = new FileOutputStream("/Users/zhongziqiang/Downloads/33_1_copy.png");  
  
        // 创建一个字节数组,长度为1024, 用于存储读取到的数据(存储1KB数据)  
        byte[] buffer = new byte[1024];  
        int len; // 存储读取到的字节数  
  
        // 循环读取文件输入字节流对象中的数据到buffer,并从buffer写入文件输出字节流对象中  
        while ((len = is.read(buffer)) != -1) {  
            fos.write(buffer, 0, len);  
        }  
  
        // 关闭文件输入字节流对象和文件输出字节流对象  
        fos.close();  
        is.close();  
    }  
}

IO流异常处理

IO流操作可能抛出各类异常,Exception甚至有一类专门的IO异常类IOException。

IO流操作过程中发生异常,需要处理异常,并且需要在各类场景下,要调用close()方法关闭IO流。因此Java提供try-catch-finnally结构处理IO流异常。

下方是使用输入、输出字节流复制文件的程序,捕获了创建字节输入和输出流可能抛出的FileNotFoundException异常、读取和写入数据可能抛出的IOException异常、关闭流可能抛出的IOException异常等。

java 复制代码
public class App3 {  
        public static void main(String[] args) {  
            String sourceFile = "/Users/zhongziqiang/Downloads/Set-Interface-in-Java.webp";    // 源文件路径  
            String destFile = "/Users/zhongziqiang/Downloads/Set-Interface-in-Java_copy.webp";       // 目标文件路径  
  
            FileInputStream fis = null;  
            FileOutputStream fos = null;  
  
            try {  
                // 1. 创建字节输入流和输出流  
                fis = new FileInputStream(sourceFile);  
                fos = new FileOutputStream(destFile);  
  
                // 2. 创建缓冲区(一次读取1KB)  
                byte[] buffer = new byte[1024];  
                int bytesRead;  
  
                // 3. 循环读取并写入数据  
                while ((bytesRead = fis.read(buffer)) != -1) {  
                    fos.write(buffer, 0, bytesRead);  
                }  
  
                System.out.println("文件复制成功!");  
  
            } catch (IOException e) {  
                System.err.println("文件复制过程中发生错误: " + e.getMessage());  
                e.printStackTrace();  
            } finally {  
                // 4. 在finally块中确保流被关闭 
                // 代码块非常臃肿 
                try {  
                    if (fis != null) {  
                        fis.close();  
                    }  
                } catch (IOException e) {  
                    System.err.println("关闭输入流时出错: " + e.getMessage());  
                }  
  
                try {  
                    if (fos != null) {  
                        fos.close();  
                    }  
                } catch (IOException e) {  
                    System.err.println("关闭输出流时出错: " + e.getMessage());  
                }  
            }  
        }  
}

由于要保证异常运行时也要关闭IO流,要使用finnally代码块,又要捕获调用close()方法时的异常,并且要考虑到字节流对象变量可能是null的场景,因此finally块特别臃肿,可读性很差。

Java7引入了"try-with-resource"的结构,来代替"try-catch-finnally",可以简化代码。具体来说,就是在异常捕获try后增加一个括号,用来编写资源的定义语句。在发生异常后,系统会自动释放资源。

java 复制代码
try(/* 定义资源、发生异常时系统自动释放资源 */) {  
    // 可能发生异常的代码块  
} catch (Exception e) {  
    // 处理异常的代码块  
}

原复制文件的程序可以改造为:

java 复制代码
public class App4 {  
    public static void main(String[] args) {  
        String sourceFile = "/Users/zhongziqiang/Downloads/Set-Interface-in-Java.webp";    // 源文件路径  
        String destFile = "/Users/zhongziqiang/Downloads/Set-Interface-in-Java_copy.webp";       // 目标文件路径  
  
        try (  
                // 1. 创建字节输入流和输出流,发生异常时,fis和fos会自动关闭  
                FileInputStream fis = new FileInputStream(sourceFile);  
                FileOutputStream fos = new FileOutputStream(destFile);  
                ) {  
            // 2. 创建缓冲区(一次读取1KB)  
            byte[] buffer = new byte[1024];  
            int bytesRead;  
  
            // 3. 循环读取并写入数据  
            while ((bytesRead = fis.read(buffer)) != -1) {  
                fos.write(buffer, 0, bytesRead);  
            }  
  
            System.out.println("文件复制成功!");  
        } catch (IOException e) {  
            System.err.println("文件复制过程中发生错误: " + e.getMessage());  
            e.printStackTrace();  
        }  
    }  
}

FileReader文件输入字符流

字节流在处理纯文本文件时,由于主流字符集编码常用可变编码,比如"UTF-8"、"GBK",因此在读取字节并处理时,容易截断编码的字节,出现乱码。

对于纯文本文件的IO,Java推荐使用字符流。

对于读取计算机本地文件的文件输入字符流是FileReader实现类。与FileInputStream文件输入字节流相似,FileReader支持通过文件路径字符串(类似File对象构造方法)和通过File对象两种方式创建对象:

java 复制代码
public class App5 {  
    public static void main(String[] args) throws IOException {  
        // 方式1:通过文件路径字符串创建文件输入字符流对象  
        Reader reader = new FileReader("test.md");  
        reader.close();  
          
        // 方式2:通过文件对象创建文件输入字符流对象  
        File file = new File("test.md");  
        Reader reader2 = new FileReader(file);  
        reader2.close();  
    }  
}

与FileInputStream文件输入字节流相似,FileReader支持多种读取字符的read()方法。与FileInputStream文件输入字节流读取的最小单位是字节不同,FileReader文件输入字符流读取的最小单位是字符。并且从纯文本文件中读取字符时,使用UTF-16编码,在内存中用char存储。

java 复制代码
public class App5 {  
    public static void main(String[] args) throws IOException {  
        /*  
        从纯文本文件test.md中读取数据  
         */  
        try(Reader reader = new FileReader("test.md")) {  
            // 读取方法1:一次读取一个字符  
            int ch;  
            ch = reader.read(); // 读取一个字符,返回的字符的Unicode码点,如果没有数据,则返回-1  
            System.out.print((char) ch); // 打印一个字符  
  
            // 读取方法2:一次读取多个字符  
            char[] buffer = new char[3];  
            reader.read(buffer); // 读取多个字符到字符数组,返回读取的字符数,如果没有数据,则返回-1  
            System.out.print(buffer);  
  
            // 读取方法3:一次读取多个字符,可指定偏移量和长度  
            reader.read(buffer, 0, 3); // 读取多个字符到字符数组,返回读取的字符数,如果没有数据,则返回-1。形参1:字符数组,形参2:起始索引,形参3:读取的字符数  
            System.out.print(buffer);  
        } catch (IOException e) {  
            // 捕获异常后:打印"文件读取过程中发生错误",终止程序  
            System.out.println("文件读取过程中发生错误");  
            return;  
        }  
    }  
}

最后提醒,使用try-with-resource结构,让系统自动处理异常并关闭流。

注意:FileReader 有一个重要缺陷:它使用平台默认的字符集(如Windows中文版可能是GBK,Linux可能是UTF-8)来读取文件字节并解码为字符。它不会自动检测文件本身的编码(如UTF-8),也不支持在创建文件输入字符流对象时指定编码。这可能导致读取使用UTF-8等编码保存的文件时产生乱码。因此,在明确知道文件编码(尤其是非默认编码)的情况下,更推荐使用 InputStreamReader 并指定字符集编码,例如 new InputStreamReader(new FileInputStream("test.md"), StandardCharsets.UTF_8)

FileWriter文件输出字符流

FileWriter文件输出字符流与FileReader文件输入字符流相似,仅方向相反,是建立与本地文件的,从内存中往文件中写入数据,按照字符进行写入的管道。

与FileOutputStream文件输出字节流相似,FileWriter文件输出字符流支持通过文件路径字符串和File对象两种方式创建对象,除此之外,还支持覆盖和追加两种模式。因此,有4种构造方法:

java 复制代码
FileWriter(String fileName) // 通过文件路径字符串创建对象,默认为覆盖模式
FileWriter(String fileName, boolean append) // 通过文件路径字符串创建对象,append形参为true为追加模式
FileWriter(File file) // 通过File对象创建字节输出流对象,默认为覆盖模式
FileWriter(File file, boolean append) // 通过File对象创建字节输出流对象,append形参为true为追加模式

与FileOutputStream文件输出字节流相似,FileWriter提供三种写入文件字符数据的方法:

java 复制代码
void write(int c) // 写入一个字符到文件,c为写入字符的Unicode码点
void write(char cbuf[]) // 将char数组中的数据,写入到文件
void write(char cbuf[], int off, int len)// 将char数组中的数据,并指定写入的起始索引和结束索引
void write(String str) // 将字符串写入到文件
void write(String str, int off, int len) // 写入一个字符串,并指定写入的字符串的起始索引和结束索引

操作示例如下:

java 复制代码
public class App6 {  
    public static void main(String[] args) throws IOException {  
        Writer writer = new FileWriter("test2.md"); // 创建一个文件输入字符流对象,为覆盖模式。若文件不存在,则会创建该文件。  
  
        // 写入方式1:一次写入一个字符  
        writer.write(97); // 写入字符 a,因为a的Unicode码点为97  
  
        // 写入方式2:写入一个字符数组  
        char[] chars = {'b', 'c', 'd'};  
        writer.write(chars); // 写入字符数组 chars  
        // 写入方式3:写入一个字符数组,并指定写入的起始索引和结束索引  
        writer.write(chars, 1, 2);  
  
        // 写入方式4:写入一个字符串  
        writer.write("efg");  
  
        // 写入方式5:写入一个字符串,并指定写入的字符串的起始索引和结束索引  
        writer.write("hijk", 1, 2);  
  
        writer.close();  
        System.out.println("写入成功!");  
    }  
}

使用字符流复制纯文本文件:

java 复制代码
public class App7 {  
    /*  
    使用字符流复制一个纯文本文件  
     */    public static void main(String[] args) {  
        try(  
                Reader reader = new FileReader("/Users/zhongziqiang/Downloads/demo.md");  
                Writer writer = new FileWriter("/Users/zhongziqiang/Downloads/demo_copy.md")  
                ) {  
            char[] buffer = new char[3];  
            int len;  
            while ((len = reader.read(buffer)) != -1) {  
                writer.write(buffer, 0, len); // 只写入读取到的 len 个字符  
            }  
        } catch (IOException e) {  
            e.printStackTrace();  
            System.out.println("复制过程中发生错误");  
            return;  
        }  
    }  
}
相关推荐
你不是我我2 小时前
【Java 开发日记】我们来说一下无锁队列 Disruptor 的原理
java·开发语言
期待のcode2 小时前
Java虚拟机堆
java·开发语言·jvm
BMS小旭2 小时前
CubeMx-DMA
单片机·学习·cubemx·dma
callJJ2 小时前
WebSocket 两种实现方式对比与入门
java·python·websocket·网络协议·stomp
一条咸鱼_SaltyFish2 小时前
Spring Cloud Gateway鉴权空指针惊魂:HandlerMethod为null的深度排查
java·开发语言·人工智能·微服务·云原生·架构
i***13242 小时前
SpringCloud实战十三:Gateway之 Spring Cloud Gateway 动态路由
java·spring cloud·gateway
微露清风2 小时前
系统学习C++-第二十一讲-用哈希表封装 myunordered_map 和 myunordered_set
c++·学习·散列表
计算机徐师兄2 小时前
Java基于微信小程序的食堂线上预约点餐系统【附源码、文档说明】
java·微信小程序·食堂线上预约点餐系统小程序·食堂线上预约点餐微信小程序·java食堂线上预约点餐小程序·食堂线上预约点餐小程序·食堂线上预约点餐系统微信小程序
Chunyyyen2 小时前
【第三十周】OCR学习03
学习·ocr