【IO 流】Java 中常见的 IO 流

1. IO 流简介

何为 IO 流?

在计算机中,内存和磁盘需要进行数据传输(内存从磁盘中读入数据,进行处理,然后写入磁盘中),而数据传输需要通道。所以,这里的 “IO 流”就是数据传输的通道。

内存和磁盘数据交互图如下:

在这里插入图片描述
大致意思:内存通过输入流从磁盘中读取数据,这个过程也称为读;内存通过输出流将数据保存到磁盘中,这个过程也称为写。

所以,简单来说,IO 流的作用就是 通过 IO 流,可以完成对磁盘文件的读和写。

2. IO 流的分类

  1. 按流的方向分(以内存为参照物):输入流(往内存中去)、输出流(从内存中出来)
  2. 按数据的读取方式:字节流、字符流

字节流和字符流的区别?

  1. 字节流:按照字节方式读取,一次读一个字节,可以读取任意类型的文件。如:文本文件、图片、视频文件,等…
  2. 字符流:按照字符方式读取,一次读取一个字符。它是为了更方便读取普通的文本文件,无法读取图片、word文件、视频文件等…

举个例子:

有一个 test.txt 文件,它里面的内容是:a中国

用字节流读:

第一次读:读一个字节,正好读到 ‘a’
第二次读:读一个字节,正好读到 ‘中’ 的一半
第三次读:读一个字节,读到 ‘中’ 的另一半

Tips:在 Windows 系统中,字母只占一个字节(在 Java 中,字母(char) 占用两个字节,但这个文件与 Java 无关,它只是 Windows 操作系统上的一个文件),而汉字占两个字节。

用字符流读:

第一次读:读一个字符,正好读到 ‘a’
第二次读:读一个字节,正好读到 ‘中’

3. Java 中的 IO 流

Java 中的 IO 流都在 java.io.* 包下,这块被称为“四大家族”(所有的 IO 类只有四类),四大家族的首领(这四类 IO 流的顶级父类)如下:

  • java.io.InputStream:字节输入流
  • java.io.OutputStream:字节输出流
  • java.io.Reader:字符输入流
  • java.io.Writer:字符输出流

特点:

  1. 四大家族的首领都是抽象类
  2. 所有的 IO 流都实现了 java.io.Closeable 接口,都是可关闭的(close()方法),用完之后要关闭,不然会占用很多资源
  3. 所有的输出流都实现了 java.io.Flushable 接口,都是可刷新的(flush() 方法),用完之后,记得刷新

Java 中需要掌握的 IO 流有16个,如下:

文件专属(操作文件):

  • java.io.FileInputStream
  • java.io.FileOutputStream
  • java.io.FileReader
  • java.io.FileWriter

转换流(将字节流转换为字符流):

  • java.io.InputStreamReader
  • java.io.OutputStreamWriter

缓冲专属:

  • java.io.BufferedInputStream
  • java.io.BufferedOutputStream
  • java.io.BufferedReader
  • java.io.BufferedWriter

数据流专属:

  • java.io.DataInputStream
  • java.io.DataOutputStream

标准输出流:

  • java.io.PrintStream
  • java.io.PrintWriter

对象专属流:

  • java.io.ObjectInputStream
  • java.io.ObjectOutputStream

这里重点以“FileInputStream/FileOutputStream”举例,因为其它流与之相似

3.1 FileInputStream/FileOutputStream

3.1.1 FileInputStream

FileInputStream:文件字节输入流

FileInputStream 类的 read() 方法有三个重载形式:

  1. public int read():读取单个字节,返回值为 int 类型,代表用该字节编码的的 ascii 码值
  2. public int read(byte [] b, int offset, int len):读取若干字节到字节数组中,返回值为读取的字节数
  3. public int read(char cbuf[]):它的父类 Reader的方法。读取若干字节到字节数组中,返回值为读取的字节数

场景:使用文件字节流读取磁盘中的一个文件temp.txt,其内容为:abcdef

示例一: 读取文件内容(英文),一个字节一个字节地读取。

public class FileStreamDemo {

    public static void main(String[] args) {
        // 文件绝对路径名
        String name = "E:\\zzc\\temp.txt";
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(name);

            // 开始读
            // read() 方法返回的是:读取到的“字节”本身。
            // 例如:读取到的字符是'a',则返回97
            int data = fis.read();
            System.out.println(data); // 97

            data = fis.read();
            System.out.println(data); // 98

            data = fis.read();
            System.out.println(data); // 99

            data = fis.read();
            System.out.println(data); // 100

            data = fis.read();
            System.out.println(data); // 101

            data = fis.read();
            System.out.println(data); // 102

            // 读取到文件末尾返回 -1
            data = fis.read();
            System.out.println(data); // -1
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != fis) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意:如果文件中的内容包含中文,再一个字节一个字节地读取,那么输出出来会出现乱码。因为,在 Java 中,汉字至少用了 2 个字节编码。如果按照上面示例,一个字节一个字节地输出,那么,这样只是截取了汉字的一部分,肯定会乱码。

【扩展知识】:

内码Stringchar 运行在内存中的编码
外码:!内码。如:class 文件中的 char/String 类型、序列化的 char/String 类型

UTF-8:英文字符占用一个字节;绝大多数汉字占用三个字节,个别汉字占用四个字节
UTF-16:英文字符占两个字节;绝大多数汉字(尤其是常用汉字)占用两个字节,个别汉字占用四个字节

在 Java 中,内码使用 UTF-16;外码使用 UTF-8。char 类型数据占用 2 个字节是针对内码的。

temp.txt 文件中只有一个汉字“我”,然后用

try (FileInputStream in = new FileInputStream("temp.txt")) {
    int len = in.read();
    System.out.println(len);
    len = in.read();
    System.out.println(len);
    len = in.read();
    System.out.println(len);
} catch (IOException e) {
    e.printStackTrace();
}

它会输出:

230
136
145

结果表明:汉字“我”用了3个字节编码,每一个字节对应的 ASCII 码值分别为:230、136、145。

示例二: 读取文件内容(英文)。循环地一个字节一个字节地读取。

如果一个文件内容巨多,照上面那种方式肯定就不行了。所以,下面用循环的方式进行读取。

public class FileStreamDemo {

    public static void main(String[] args) {
        // 文件绝对路径名
        String name = "E:\\zzc\\temp.txt";
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(name);
            int data = 0;
            while ((data = fis.read()) != -1) {
                System.out.println(data);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != fis) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

但是,上面还会有一个问题:就是一次只读取一个字节,那么要读取所有字节,就要读取多次。这就会意味着内存要和磁盘频繁交互,这样性能会严重降低。所以,就要考虑一次能否读取多个字节。

查看 API 文档,可知:调用 read(byte[] b) 方法

示例三: 读取文件内容(英文)。多个字节多个字节地读取

public class FileStreamDemo {

    public static void main(String[] args) throws Exception{
        // 文件绝对路径名
        String name = "E:\\zzc\\temp.txt";
        FileInputStream fis = new FileInputStream(name);
        byte[] bytes = new byte[4];
        
        int length = 0;
        while ((length = fis.read(bytes)) != -1) {
            // 将 byte 数组转换成字符串
            System.out.print(new String(bytes, 0, length));
        }

        if (null != fis) {
            fis.close();
        }
    }
}

说明:

  1. new String(bytes, 0, length):将字节转换为字符串

【总结】:使用文件字节输入流读取文件有三种:

  1. int len = in.read(); :一次只读取一个字节,返回值是该字节对应的 ASCII 码值。如:读取了一个英文字符 I,则它返回 73。因为大写的 I 对应的 ASCII 码为 73。但是,如果读取的是一个汉字,则需要读取三次才能读完,因为常用的汉字用 3 个字节编码,不常用的汉字用 4 个字节编码。
  2. while ((len = in.read()) != -1) {}:一次只读一个字节,然后,循环读取。
  3. byte[] bytes = new byte[1024]; while ((len = in.read(bytes)) != -1) {}:循环多个字节读取

3.1.2 FileOutputStream

FileOutputStream:文件字节输出流

FileOutputStream 类的 write() 方法有四个重载形式:

  1. public void write(int b):将指定的字节写入到文件输出流
  2. private native void write(int b, boolean append):是否追加
  3. public void write(byte b[]):将字节数组写入到文件输出流
  4. public void write(byte b[], int off, int len):将字节数组中的一部分写入到文件输出流

场景:使用文件输出流向磁盘某个文件写入数据

示例一:

public class FileOutputStreamDemo {

    public static void main(String[] args) throws Exception{
        // temp.txt 文件不存在时,会自动新建
        FileOutputStream fos = new FileOutputStream("temp.txt");
        byte[] bytes = {97, 98, 99, 100};

        fos.write(bytes);
        // 写完之后,一定要刷新
        fos.flush();
        fos.close();
    }
}

注意:如果文件不存在,则会新建文件。但是有一个前提,此文件所在的父级目录必须存在,否则,就会报错!!

如:FileOutputStream fos = new FileOutputStream("E:/zzc/temp.txt");

如果 E 盘下面没有 zzc 文件夹,那么,这行代码就会报错 java.io.FileNotFoundException: E:\zzc\temp.txt (系统找不到指定的路径。)。所以,必须保证文件的父级目录存在。

而且每次向此文件中写入数据之前,都会清空此文件。

那么,每次向此文件写入数据时,能不能向此文件中追加内容呢?

使用这个构造方法:

  • FileOutputStream(String name, boolean append):创建一个向具有指定 name 的文件中写入数据的输出文件流。如果第二个参数为 true,则将字节写入文件末尾处,而不是写入文件开始处。

如何向文件中写入汉字呢?

使用如下方式:

String data = "我爱中国";
// 将字符串转换为字节数组
byte[] bytes = data.getBytes();
fos.write(bytes);

说明:

  1. byte[] bytes = data.getBytes();String 转换为字节类型

3.1.3 使用文件字节流

场景:使用文件字节流实现文件的赋值:把文件 temp.txt 中的内容复制到 test.txt 文件中去。

示例:

public class FileStreamDemo {

    public static void main(String[] args) throws Exception{
        FileInputStream fis = new FileInputStream("E:\\zzc\\temp.txt");
        FileOutputStream fos = new FileOutputStream("E:\\zzc\\test.txt");

        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }

        if (null != fos) {
            fos.flush();
        }
        if (null != fis) {
            fis.close();
        }
        fos.close();
    }
}

不想手动关闭流,即:调用 close() 方法,可使用 try-with-resources 结构改进,如下:

public class FileStreamDemo {

    public static void main(String[] args) throws Exception{
        FileInputStream fis = new FileInputStream("E:\\zzc\\temp.txt");
        FileOutputStream fos = new FileOutputStream("E:\\zzc\\test.txt");

        try (FileInputStream in = new FileInputStream("E:\\zzc\\temp.txt");
        FileOutputStream out = new FileOutputStream("E:\\zzc\\test.txt")) {
            int length = 0;
            byte[] bytes = new byte[1024];
            while ((length = in.read(bytes)) != -1) {
                out.write(bytes, 0 , length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

如果还有不清楚 try-with-resources 结构的同学,可查看博文:【Exception】如何使用 try-with-resources 优雅地关闭资源

3.2 FileReader/FileWriter

3.2.1 FileReader

FileReader:文件字符输入流,只能读取普通文本,方便、快捷

FileReader 类的 read() 方法有两个重载形式:

  1. public int read():读取单个字符,返回值为 int 类型,代表该字符的 ascii 码值
  2. public int read(char [] c, int offset, int len):读取若干字符到字符数组中,返回值为读取的字符数
  3. public int read(char cbuf[]):它的父类 Reader的方法。读取若干字符到字符数组中,返回值为读取的字符数

场景:使用文件字符输入流读取一个txt文件(包含中文)

示例:

public class FileReaderDemo {

    public static void main(String[] args) throws Exception{
        FileReader fr = new FileReader("temp.txt");

        char[] chars = new char[4];
        int length = 0;
        while ((length = fr.read(chars)) != -1) {
            System.out.print(new String(chars, 0, length));
        }
        fr.close();
    }
}

3.2.2 FileWriter

FileWriter:文件字符输出流,只能写普通文本

FileWriter 的父类 Writerwrite() 方法重载形式:

  1. public void write(int c)
  2. public void write(char cbuf[])
  3. public void write(char cbuf[], int off, int len)
  4. public void write(String str)
  5. public void write(String str, int off, int len)

场景:使用文件字符输出流向一个txt文件写入数据(包含中文)

public class FileWriterDemo {

    public static void main(String[] args) throws Exception {
        FileWriter fw = new FileWriter("fw.txt");
        
        char[] chars = {'我', '是', '中', '国', '人'};
        fw.write(chars);
        fw.flush();
        fw.close();
    }
}

3.3 BufferedReader/BufferedWriter

3.3.1 BufferedReader

BufferedReader:带有缓冲区的字符输入流。使用此流时,不需要自定义 byte/char 数组。自带缓冲。

示例一:

public class BufferedReaderDemo {

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new FileReader("temp.txt"));

        String line = null;
        while (null != (line = br.readLine())) {
            System.out.print(line);
        }

        br.close();
    }
}

示例二:

public class BufferedReaderDemo {

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("temp.txt")));

        String line = null;
        while (null != (line = br.readLine())) {
            System.out.print(line);
        }

        br.close();
    }
}

示例一与示例二的区别:示例二中使用了字节流来读取 temp.txt,但 BufferedReader 的构造方法中只能传入 Reader 类型的参数。所以,这里使用了 字节输入流来转换。

这里 BufferedWriter 就不说了。

3.4 DataOutputStream/DataInputStream

DataOutputStream:此流可以将数据以及数据类型写入文件中。
注意:此文件不是普通文件(无法用记事本打开)

3.5 PrintStream/PrintWriter

此类流是标准输出流。

3.5.1 PrintStream

PrintStream 是字节输出流,默认输出到控制台。

示例:

public class PrintStreamDemo {

    public static void main(String[] args) {
        String msg = "Hello PS";
        PrintStream out = System.out;
        // System.out.println(msg);
        out.println(msg);
    }
}

既然是默认输出到控制台。那么可以改变标准输出流的方向吗?

当然可以。

public class PrintWriterDemo {

    public static void main(String[] args) throws Exception{
        // 标准输出流不再指向控制台,而是指向 log 文件
        PrintStream ps = new PrintStream(new FileOutputStream("log"));
        // 修改输出方向,将输出方向从控制台修改为 log 文件
        System.setOut(ps);
        ps.print("我是中国人");
    }
}

3.5.2 PrintWriter

示例:

public class PrintWriterDemo {

    public static void main(String[] args) throws Exception{
        // 若此文件不存在,则创建
        PrintWriter pw = new PrintWriter("pw.txt");
        String data = "Hello PrintWriter";
        pw.write(data);

        // 使用字符输出流,则需要调用 flush() 方法
        // 字符内部也是用字节。相当于字符内部有缓存区,如果不调用 flush() 方法,那么数据只会存留在缓冲区中,
        // 并不会输出到文本中去
        pw.flush();
        // 当调用 close() 时,它会先调用 flush() 方法。即使不显示调用 flush() 方法
        pw.close();
    }
}

3.6 ObjectOutputStream/ObjectInputStream

主要用于:对象的序列化和反序列化

示例(序列化):

public class User implements Serializable {
    private static final long serialVersionUID = -2002612691013855369L;

    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    ...
}

User 类实现了 Serializable 接口

public class ObjectOutputStreamDemo {

    public static void main(String[] args) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.txt"));
        User user = new User("zzc", 18);
        oos.writeObject(user);

        oos.flush();
        oos.close();
    }
}

注意:参与序列化和反序列化的对象都要实现 Serializable 接口。

3.7 关于内存的流

public class MemoryStream {

    public static void main(String[] args) throws Exception{
        // 1. 内存字节流
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        String data = "Hello Memory";
        // 将数据写入内存
        baos.write(data.getBytes());
        // 获取内存中的数据
        byte[] bytes = baos.toByteArray();
        //System.out.println(new String(bytes));
        baos.close();

        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        int ch;
        while ((ch = bais.read()) != -1) {
            System.out.print((char) ch);
        }
        bais.close();


        // 2. 内存字符数组流
        CharArrayWriter caw = new CharArrayWriter();
        char[] chs = {'H', 'e', 'l', 'l', 'o'};
        caw.write(chs);
        caw.close();

        // 返回内存中数据的引用
        char[] data = caw.toCharArray();
        CharArrayReader car = new CharArrayReader(data);
        int ch;
        while ((ch = car.read()) != -1) {
            System.out.print((char) ch);
        }
        car.close();


        // 3. 内存字符串流
        StringWriter sw = new StringWriter();
        String data = "Hello World";
        sw.write(data);
        sw.close();

        String s = sw.toString();
        StringReader sr = new StringReader(s);
        int ch;
        while((ch = sr.read()) != -1) {
            System.out.print((char) ch);
        }
        sr.close();
    }
}

好了,一些流的基本使用就到这儿了。多看看它们的 API 文档吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值