My Blog

IOStream

记录 File class 和 IO Stream

学习笔记,仅供参考

参考B站Mirco_Frank - java 进阶 | 官方文档英文版



目录


🧷 File class

File 类是对一个文件夹或文件路径的抽象表示,文件路径可以是 绝对路径 (absolute path)抑或是 相对路径 (relative path)

不同操作系统下,文件路径的分隔符会不同,Unix 下用 /; 而 Win 10 下用 \\

构造器

File(String pathname)   // 将所给的文件名转为抽象路径

方法

练习 file class

  • canExecute() | canRead() | canWrite()   // 判断该文件是否可执行 | 可读 | 可写

  • length()   // 返回所给文件的字符长度

  • exists()   // 判断所给的文件是否存在

  • isAbsolute() | isDirectory() | isFile() | isHidden()   // 判断文件路径是否为绝对路径 | 该文件是否是文件夹 | 是否是文件 | 是否是隐藏文件

  • createNewFile()   // 返回 true 表明该文件不存在,以该文件名为名创建新的空文件;返回 false 表明该文件已存在;用时还要处理 IOException

  • delete()   // 删除该文件/文件夹

  • mkdir()   // 以该抽象路径名创建文件夹

  • getCanonicalFile()   // 获取该文件的规范形式文件,即绝对路径且唯一的,与操作系统有关;同样要处理异常

  • getAbsolutePath()   // 获取该文件的绝对路径字符串

  • getName()   // 获取该文件/文件夹的名

  • getParent()   // 返回此文件的父目录的路径名字符串,如果此路径名未命名父目录,则返回 null。

  • getPath()   // 获取文件的路径名

  • list()   // 遍历获取此抽象路径下的所有文件和子文件夹,并以字符串的形式返回

  • listFile()   // 遍历获取此抽象路径下的所有文件和子文件夹,并以 File 的形式返回


🌠 I/O Stream

I: input source;   O: output distination;   Stream: the process/pipe (from source to dist or from dist to source) and it support many different kinds of data, include: bytes, primitive data types, localized characters, and objects.

官方教程解释 I/O Stream

下面以水库输水来说明流的过程

Water_stream_exp

输入输出是有相对性的,可以从 src 到 dist, 或者 dist 到 src

接着来探讨程序中的 IO 流概念,在程序中流就是一串数据,当程序要 从源读取数据 时就要使用 input; 当程序要 向源写入数据 时就要使用 output。所以一切都是以程序为主,以它作为主角,输入输出都是相对它而言的。

IO

图片来自官网文档

从图片中也可以看出,数据源/数据目标可以有很多种形式:另一个程序、文件、数据库等


字节流 InputStream 和 OutputStream

InputStream 是抽象类,它是所有字节输入类的父类;同样,OutputStream 是所有字节输出类的父类。所有的 字节流类 都是继承自他俩

InputStream 的常用方法:close() - 当程序不在读数据时,关闭输入流;read() - 从输入流中读取下个字节

OutputStream 的常用方法:close() - 当程序不在写数据时,关闭输出流;write(int b) - 写字节到输出流中

因为抽象类的子类会重写父类的方法,所以上面的方法也是其子类常用的


FileInputStream 和 FileOutputStream

它俩用于读文件或写文件,通过字节的形式来完成。

public class Test {
    public void fileInputStreamTest() throws IOException {
        // 创建文件流
        FileInputStream in = new FileInputStream("xx/text.txt");

        try {            
            int c;
            // 读取文件流源文件中的数据到程序中
            while ((c = in.read()) != -1) {
                // 将从文件流中获取的字节强转为 char 型
                // 然后将其输入
                System.out.print((char) c);

        } finally {
           // 读取之后,关闭所用的文件流
           if (in != null) {
               in.close();
           }
       }
    }
}

其中用到 read() 方法,它返回下个数据的字节,要是到底了就返回 -1;还有不论如何最后都要关流,以防止数据泄露

public class Test {
    public void fileOutputStreamTest() throws IOException {
        // 创建输出流
        FileOutputStream out = new FileOutputStream("xx/output.txt");

        try {
            // 因为是字节流写入,所以要将所写的 String 转为字节数组
            byte[] bytes = "Hello World!".getBytes();
            // 将程序中的字节写入到输出流的目标文件中
            out.write(bytes);

        } finally {
            if (out != null) {
                // 写好之后,关闭输出流
                out.close();
            }
        }
    }
}

write() 方法将数据写入到目标文件中,放还有几个重载的方法,以满足不同字节类型输入

有了读取和写入,就可以满足对文件的读写要求了,后面只是对性能的提升。通过读写还可以完成文件的 copy 操作

public class Test {
    public void fileCopyTest() throws IOException {
        // 创建输入输出流
        FileInputStream in = new FileInputStream("xx/input.txt")
        FileOutputStream out = new FileOutputStream("xx/output.txt");

        // 从 src 中读向 dist 中写
        try {
            int c;
            // 单字节读取
            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            // 关流
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

上面所用的是单个字节来进行数据的读写,就像搬砖一样,一次只搬一块砖就会显得效率太低。所以引入一个 byte[] 类型的 buffer 来增加一次搬运的容量,就像一次搬十块砖一样,效率就会提高很多。下面就来看看代码的实现

public class Test {
    public void fileCopyTest() throws IOException {
        // 创建输入输出流
        FileInputStream in = new FileInputStream("xx/input.txt")
        FileOutputStream out = new FileOutputStream("xx/output.txt");

        // 读取数据到缓存区,缓存区大小为 1KB
        try {
            byte[] buff = new byte[1024];
            int c;
            
            while ((c = in.read(buff)) != -1) {
                out.write(buff, 0, c);
            }
        } finally {
            // 关流
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

从上述代码中,可以看到 buffer 的大小被设置为 1024 bytes(1KB),同时还用到了 read(byte[] b)write(byte[], int off, int len) 的重载方法,

  • read(byte[] b)   // 读取数据到 byte 数组中,并返回所读到的总字节数(即 1024, 数组的大小),若没读到就返回 -1

  • write(byte[] b, int off, int len)   // 将字节数组写到输出流中,off 指要从数组的哪开始写,len 指要写入多大的字节

BufferedInputStream 和 BufferedOutputStream

Java 作为高级语言,自然有直接可以用的字节缓存读写类,它们就是 BufferedInputStream 和 BufferedOutputStream ,其实是它们的内部新建了一个 byte 数组,原理上跟上面的差不多,只是它们使用起来要更加地方便

public class Test {
    public void fileCopyTest() throws IOException {
        // 创建字节缓存输入输出流
        BufferedInputStream buffIn = new BufferedInputStream(new FileInputStream("xx/input.txt"));
        BufferedOutputStream buffOut = new BufferedOutputStream(new FileOutputStream("xx/output.txt"));

        // 读取数据
        try {
            int c;

            while ((c = buffIn.read()) != -1) {
                buffOut.write(c);
            }
        } finally {
            // 关流
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }

    }
}

由于 BufferedInputStream(InputStream in) 构造器要接受一个输入流,所以用到 new FileInputStream("xx/input.txt") 来生成一个文件输入流,output 同理。本来用 FileInputStream 就可以完成读写,为了让读写更加高效,相当于给 FileInputStream 对象开了个 VIP 一样。所以要想开 VIP,你得先要注册账号(FileInputStream 对象),在设计模式中这种类似升级 VIP 的服务叫 装饰设计模式

心血来潮,顺便简要测试了单字节读写、手写缓存区读写、自带缓存区读写三种读写所消耗的时间

spend-time

由图可知,缓存区读写大约是单字节读写的十分之一,而手写的缓存区要稍慢于自带的,可能是内部建的 byte 数组底层优化的好


字符流 Reader & Writer

除了字节流外,还有字符流,但实质上字符还是用字节来表示。为了更方便地处理字符文本文件,所以有了 Reader & Writer。与之前的 InputStream & OutputStream 一样,它们也是抽象的,所有的 字符流 类都继承自他俩。

通常字符流用来读写文本文件更方便,字节流来读写视频、音频、图片等格式的文件更方便,详细查看字符流与字节流的区别

同样提供了一些读写必要的方法 close(), read(), write()

FileReader & FileWriter

它俩具体实现 Reader & Writer 类

public class Test {
    public void fileReaderTest() throws IOException {
        // 新建输入流
        FileReader fr = new FileReader("xx/input.txt");
        // 读取数据
        int c;
        while ((c = fr.read()) != -1) {
            System.out.print((char) c);
        }
        // 关流
        if (fr != null) {
            fr.close();
        }
    }
}

/**********************分割线*********************/

public class Test {
    public void fileWriterTest() throws IOException {
        // 创建输出流
        FileWriter fr = new FileWriter("xx/output.txt");
        // 写入数据(直接将要写的字符串丢进去即可)
        fw.write("hello world");
        // 关流
        if (fw != null) {
            fw.close();
        }
    }
}

/***************分割线********************/

// 复制的操作与字节流复制的一样,也是边读边写
// 只不过只能读写文本文件
    

同样,为了让读写更高效,有了 BufferedReader & BufferedWriter

public class Test {
    public void bufferedReaderTest() throws IOException {
        // 新建输入流
        BufferedReader buffReader = new BufferedReader(new Reader("xx/input.txt"));
        // 以字符串的形式读取数据
        String str;
        while ((str = buffReader.readLine()) != null) {
            System.out.println(str);
        }
        // 关流
        if (buffReader != null) {
            buffReader.close();
        }
    }
}

BufferedReader 可以使用 readLine() 方法来读取,要打印时就要是用 System.out.println() 来打印,否则文本中的回车换行就会不显示

public class Test {
    public void bufferedWriterTest() throws IOException {
        // 新建输入流
        BufferedWriter buffWriter = new BufferedWriter(new Writer("xx/output.txt"));
        // 以字符串的形式写入数据
        buffWriter.write("hello");
        buffWriter.newLine();  // 用该方法写入换行,防止不同系统的换行符不同而引起错误
        buffWriter.write("world!");
        // 关流
        if (buffWriter != null) {
            buffWriter.close();
        }
    }
}

复制的话也是类似的,下面仅列出读写数据的部分

// 定义字符串变量来接收读取到的数据
String str;
while ((str = buffReader.readLine()) != null) {
    buffWriter.write(str);
    buffWriter.newLine();  // 每写一行数据换一行
}

转换流 (InputStreamReader & OutputStreamWriter)

它俩是作为字节和字符相互转换的桥梁,通过 InputStreamReader 可以将文件中的字节转为字符,然后进行读取;同样,OutputStreamWriter 是将写入的字符转为字节,然后放入文件中进行存储。

使用场合:

  1. 源或目标所对应的设备是字节流,但操作的确实文本数据,可以使用其来作为桥梁

  2. 一旦操作的文本涉及到具体的指定编码表时,必须使用转换流

下面以系统的输入输出流(字节)读写到文本文件为例

public class Test {

    /**
     *  读取系统输入(字节)
     *  写入到文件中(字符)
     */

    public void readTest() throws IOException {
        // 将系统输入流转为字符流进行读取
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter writer = new BufferedWriter(new FileWrite("xx/output.txt"));

        // BufferedReader 的 readLine() 获取的是字符串
        String data;
        // 当读取系统输入为 “exit” 时,停止读取
        while (! "exit".equals(data = reader.readLine())) {
            writer.write(data);
            writer.newLine();
        }

        // 关流
        if (writer != null) {
            writer.close();
        }
        if (reader != null) {
            reader.close();
        }
    }

/*-----------------------分割线-----------------------*/

    /**
     *  从文件中读取数据(字符)
     *  写入数据到系统输出(字节)
     */

    public void writeTest() throws IOException {
        // 将系统输入流转为字符流进行读取
        BufferedReader reader = new BufferedReader(new FileReader("xx/input.txt"));
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
        
        // BufferedReader 的 readLine() 获取的是字符串
        String data;
        // 读写数据
        while ((data = reader.readLine()) != null) {
            writer.write(data);
            writer.newLine();
        }

        // 关流
        if (writer != null) {
            writer.close();
        }
        if (reader != null) {
            reader.close();
        }
    }
}

数据流 (DataInputStream & DataOutputStream)

数据流支持 Java 的基本类型读写,但要注意的是,所读的文件必须是用数据流 DataOutputStream 写入的数据文件,不然会造成 字符编码问题

例子来源于官网教程

// 定义一个全局变量的文件名, 没有后缀,即使给了后缀打开也是乱码
static final String dataFile = "invoicedata"

/**
 *  用数据流写入数据
 */

public void dataWriteStream() throws IOException {
        // 定义一些要写入的变量
        final double[] prices = {19.99, 9.99, 15.99, 3.99, 4.99};
        final int[] units = { 12, 8, 13, 29, 50 };
        final String[] descs = {
                "Java T-shirt",
                "Java Mug",
                "Duke Juggling Dolls",
                "Java Pin",
                "Java Key Chain"
        };

        // create out stream and write data into specify file
        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(dataFile)));
        for (int i = 0; i < prices.length; i++) {
            out.writeDouble(prices[i]);
            out.writeInt(units[i]);
            out.writeUTF(descs[i]);
        }

        // 关流
        if (out != null) {
            out.close();
        }
    }

/*******************分割线*********************/

/**
 *  用数据流读取写好的数据
 */

public void dataReadTest() throws IOException {
        // create in stream and read data from specify file
        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(dataFile)));
        // 定义接受不同类型数据的变量
        double price;
        int unit;
        String desc;

        try {
            while (true) {
                // 读取数据的类型要与写入时的保持一致
                price = in.readDouble();
                unit = in.readInt();
                desc = in.readUTF();
                System.out.format("You ordered %d" + " units of %s at %.2f%n", unit, desc, price);
            }
        } catch (EOFException e) {
            e.printStackTrace();
        }
    }

注意:

DataStreams 通过捕获 EOFException 来检测文件结束条件,而不是测试无效的返回值。


对象流 (ObjectInputStream & ObjectOutputStream)

跟数据流一样,对象流支持读写对象,通过 ObjectOutputStream 将对象序列化,然后存储到磁盘中或发布到网上,用户再通过 ObjectIntputStream 将数据反序列化为对象,然后操作使用。所以想要用对象流,那么该对象就必须是 实现 Serializable 接口类的实例

跟数据流相比,对象流的数据类型就更加的丰富了,不仅有基本的数据类型,还可能包含其他的对象。下面仍然是订单的例子,只不过它的价格属性用的是 Math.BigDecimal 类,BigDecimal 类是用来计算 更高精度要求的小数,比如商业的金钱交易,使用它的算术方法来进行加减乘除运算,而不是用运算符运算。下面的例子来源于官方教程

public class ObjectStreams {
    // 定义数据
    static final String dataFile = "invoicedata";

    static final BigDecimal[] prices = { 
        new BigDecimal("19.99"), 
        new BigDecimal("9.99"),
        new BigDecimal("15.99"),
        new BigDecimal("3.99"),
        new BigDecimal("4.99") };
    static final int[] units = { 12, 8, 13, 29, 50 };
    static final String[] descs = { "Java T-shirt",
            "Java Mug",
            "Duke Juggling Dolls",
            "Java Pin",
            "Java Key Chain" };

    public static void main(String[] args) 
        throws IOException, ClassNotFoundException {

        // 对象流写数据到文件
        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(new
                    BufferedOutputStream(new FileOutputStream(dataFile)));
            // 写入日期、价格、数量、名称到文件
            out.writeObject(Calendar.getInstance());
            for (int i = 0; i < prices.length; i ++) {
                out.writeObject(prices[i]);
                out.writeInt(units[i]);
                out.writeUTF(descs[i]);
            }
        } finally { // 关流
            out.close();
        }

/********************分割线****************************/

        // 对象流从文件中读数据
        ObjectInputStream in = null;
        try {
            in = new ObjectInputStream(new
                    BufferedInputStream(new FileInputStream(dataFile)));
            // 定义接收数据的变量
            Calendar date = null;
            BigDecimal price;
            int unit;
            String desc;
            BigDecimal total = new BigDecimal(0);
            // 先接收日期
            date = (Calendar) in.readObject();

            System.out.format ("On %tA, %<tB %<te, %<tY:%n", date);
            // 跟数据流一样按照写入时的顺序读数据
            try {
                while (true) {
                    price = (BigDecimal) in.readObject();
                    unit = in.readInt();
                    desc = in.readUTF();
                    System.out.format("You ordered %d units of %s at $%.2f%n",
                            unit, desc, price);
                    total = total.add(price.multiply(new BigDecimal(unit)));
                }
            } catch (EOFException e) {} // 同样,等捕获到异常后退出循环
            System.out.format("For a TOTAL of: $%.2f%n", total);
        } finally { // 关流
            in.close();
        }
    }
}