Java I/O 流详解(Basic I/O)

目录

1、Java Basic I/O 中的字节流:Byte Streams

2、Java Basic I/O 中的字符流:Character Streams

3、Java Basic I/O 中的缓冲流:Buffered Streams

4、Java Basic I/O 中的打印流:PrintStream (数据扫描和格式化)

5、Java Basic I/O 中的数据流:Data Streams

6、Java Basic I/O 中的对象流:Object Streams


        一个 I/O 流代表一个输入源或输出目的地。一个流可以表示许多不同类型的输入源和输出目的地,比如磁盘文件、设备、其他程序和存储阵列。

        流支持许多不同类型的数据,比如字节(字节流)、基本数据类型(数据流)、字符串(字符流)和对象(对象流)。有些流只是简单地传递数据;而有些流则能以有效的方式使用和转换数据。

        不管流在内部是如何工作的,所有流在程序中都是一个数据序列。程序使用输入流从输入源读取数据,而且一次只读取一个流:

         程序使用输出流将数据写入输出目的地,每次也是写入一个流:

        上图中的数据源和输出目的地可以是保存、生成或使用数据的任何东西,比如括磁盘文件,但数据源或输出目的也可以是另一个程序、外围设备、网络套接字或存储阵列等

        Java 中的 IO 流汇总图示:

1、Java Basic I/O 中的字节流:Byte Streams

        程序使用字节流来执行 8-bit bytes 的输入和输出(8 个二进制位表示为 1 个字节)。所有的字节流都起源于 InputStream 和 OutputStream。Java 中有许多的字节流,本节使用 Java 中的文件字节流(FileInputStream 和 FileOutputStream)进行演示,其他字节流的使用方式大致相同;它们的主要区别在于流的构造方式。

        下边的程序使用 FileInputStream 和 FileOutputStream 读取和复制 xanadu.txt 文件:

public class CopyBytes {

    public static void main(String[] args) throws IOException {
        FileInputStream in = null;
        FileOutputStream out = null;
        try {
            in  = new FileInputStream("F:\\xanadu.txt");
            out = new FileOutputStream("F:\\outagain.txt");
            int c;
            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if (in != null) { // 判断流是否存在,如果文件打开错误,那么流始终是null值
                in.close(); // 关闭流,避免严重的资源泄漏
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

        CopyBytes 会将大部分时间花费在读取输入流和写入输出流的简单循环中,每次一个字节,如下图所示:

        CopyBytes 看起来是一个普通的程序,但它实际上代表了一种应该避免使用的低级 I/O。由于 xanadu.txt 文件中包含的是字符数据,所以最好的方法是使用字符流,那么为什么还要存在字节流呢?那是因为所有其他流类型都是建立在字节流的基础上

2、Java Basic I/O 中的字符流:Character Streams

        Java 存储字符值使用 Unicode 字符集进行编码,字符流 I/O 自动将这种字符编码应用到本地字符串的转换过程中,在西方地区,本地字符集通常使用 ASCII 进行编码。// 编码不统一会导致乱码问题

        所有的字符流类都起源于 Reader 和 Writer。与字节流一样,字符流中也有专门处理文件 I/O 的字符流类:FileReader 和 FileWriter。下边的程序使用 FileReader 和 FileWriter 读取和复制 xanadu.txt 文件:

public class CopyCharacters {

    public static void main(String[] args) throws IOException {
        FileReader inputStream = null;
        FileWriter outputStream = null;

        try {
            inputStream  = new FileReader("F:\\xanadu.txt");
            outputStream = new FileWriter("F:\\characteroutput.txt");

            int c;
            while ((c = inputStream.read()) != -1) {
                outputStream.write(c);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

        CopyCharacters 和 CopyBytes 类非常相似。区别在于 CopyCharacters 使用 FileReader 和 FileWriter 进行输入和输出,而不是 FileInputStream 和 FileOutputStream。需要注意的是 CopyBytes 和 CopyCharacters 都使用了 int 变量进行读写。但在 CopyCharacters 中,int 变量保存的是最后 16 bits 的字符值;在 CopyBytes 中,int 变量保存的是最后 8 bits 的字符值。// 一次读取一个字符而不是一次一个字节

        字符流通常是字节流的“包装器”。字符流使用字节流执行物理I/O,而字符流处理字符和字节之间的转换。例如,FileReader 使用 FileInputStream,而 FileWriter 使用 FileOutputStream。

        Java 提供了通用的子节流转换为字符流的桥接流类: InputStreamReader 和 OutputStreamWriter。当程序中没有预先满足需求的字符流类时,可以使用它们来创建字符流。

面向行的字符流 I/O

        字符流 I/O 通常使用比单个字符更大的单位。常见的单位是行:一行字符串,末尾带有行结束符。行结束符可以是回车符("\r")或者换行符("\n")等。

        接下来修改 CopyCharacters 的程序,会使用到两个新的类:BufferedReader 和 PrintWriter。在后文中还会具体介绍这两个类,现在我们仅关注使用他们来支持面向行的 I/O 流。

public class CopyLines {

    public static void main(String[] args) throws IOException {
        BufferedReader inputStream = null;
        PrintWriter outputStream = null;

        try {
            inputStream  = new BufferedReader(new FileReader("F:\\xanadu.txt"));
            outputStream = new PrintWriter(new FileWriter("F:\\characteroutput.txt"));

            String l;
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

        输入流调用 readLine() 方法返回一行字符串。CopyLines 使用 println() 输出每一行数据,println() 自动附加了当前操作系统的行结束符。当然,除了字符和行外,Java 还有许多方法来组织文本的输入和输出方式,比如 Scanning 和 Formatting,后边也会继续介绍到。

3、Java Basic I/O 中的缓冲流:Buffered Streams

        在上边的示例中,我们看到的都是无缓冲的 I/O。这意味着每个读或写的请求都是由底层操作系统直接处理。无缓冲的 I/O 会大大降低程序的运行效率,因为每个 I/O 请求经常会触发磁盘访问、网络活动或其他一些相对耗时的操作// 一个流的数据:字节 -> 字符 -> 行字符串 -> 缓冲区

        所以,为了减少这种开销,Java 实现了缓冲 I/O 流。缓冲输入流从内存(缓冲区)读取数据;只有当缓冲区为空时,才会调用本地输入流 API 继续读取数据。类似地,缓冲输出流将数据写入缓冲区,只有当缓冲区已满时,才会调用本地输出流 API 写入数据。// 对缓冲区进行操作

        我们可以把非缓冲流包装为缓冲流,只需将非缓冲 I/O 的对象传递给缓冲流类的构造函数就可以了。比如修改 CopyCharacters 示例中的非缓冲 I/O 为使用缓冲 I/O:

inputStream = new BufferedReader(new FileReader("F:\\xanadu.txt"));
outputStream = new BufferedWriter(new FileWriter("F:\\characteroutput.txt"));

        Java 提供了四种缓冲流包装类:BufferedInputStream 和 BufferedOutputStream 用来创建缓冲字节流,BufferedReader 和 BufferedWriter 用来创建缓冲的字符流

刷新缓冲区

        缓冲区刷新的作用就是立刻把缓冲区的数据写入存储,而不用等待缓冲区被填满。

        有些缓冲 I/O 输出类支持自动刷新,可以通过构造函数参数来进行指定。当启用自动刷新后,一些关键的事件会触发缓冲区刷新。比如,每次调用 println() 或 format() 方法的时候。如果需要手动刷新缓冲区,可以调用 flush() 方法,flush() 方法对任何类型的缓冲输出流都有效

4、Java Basic I/O 中的打印流:PrintStream (数据扫描和格式化)

        扫描器:使用 Java 提供的扫描器包装输入流读取文件中的数据,代码示例如下:// 不常用

public class ScanXan {

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

        Scanner s = null;
        try {
            // 通过控制台输入 new Scanner(System.in)
            s = new Scanner(new BufferedReader(new FileReader("F:\\xanadu.txt")));
            while (s.hasNext()) {
                System.out.println(s.next());
            }
        } finally {
            if (s != null) {
                s.close();
            }
        }
    }
}

        注意:ScanXan 类在处理 Scanner 对象时,调用了 Scanner 的 close() 方法。即使扫描器不是流,但也需要关闭它来指示本次操作已经完成,可以关闭底层流。

Formatting 格式化(打印流)

        格式化是由流对象 PrintWriter(字符流类)或 PrintStream(字节流类)的实例实现的。一般在 Java 中唯一使用打印流的地方是调用 System.out 和 System.err 方法的时候。

        与所有字节流和字符流对象一样,PrintStream 和 PrintWriter 的实例也实现了一组用于字节和字符标准输出的写入方法。此外,PrintStream 和 PrintWriter 还提供了一组将内部数据格式化输出的方法,这些方法中有两种级别的格式化:

  • print() 和 println() 以标准方式格式化单个值。
  • format() 基于格式化字符串来编排任何数量的值,有许多精确的格式化选项。

        调用 print() 或 println() 方法,会使用 toString() 方法转换值,然后输出。示例代码如下:

public class Root {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);
        
        System.out.print("The square root of ");
        System.out.print(i);
        System.out.print(" is ");
        System.out.print(r);
        System.out.println(".");

        i = 5;
        r = Math.sqrt(i);
        System.out.println("The square root of " + i + " is " + r + ".");
    }
}

        调用 format() 方法,可以使用格式化来编排值。示例代码如下:

public class Root2 {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);
        
        System.out.format("The square root of %d is %f.%n", i, r); // format
    }
}

        上述打印流在命令行环境与用户进行交互的场景中也有所涉及,Java 提供了两种命令行交互支持:标准流和控制台。因为日常使用得不是很多,所以标准流和控制台的命令行交互详情请查阅 Java 官方文档。

5、Java Basic I/O 中的数据流:Data Streams

        数据流支持基本数据类型(boolean、char、byte、short、int、long、float 和 double)和字符串类型(String)的二进制 I/O。所有的数据流都实现了 DataInput 或 DataOutput 接口。其中使用得最多的是 DataInputStream 和 DataOutputStream。

        对于 DataInputStream 和 DataOutputStream 使用的示例程序如下:

public class DataStreams {

    static final String dataFile = "F:\\invoicedata.txt";

    static final double[] prices = {19.99, 9.99, 15.99, 3.99, 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 {
        DataInputStream in = null;
        DataOutputStream out = null;
        try {
            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]);
            }
            out.flush(); // 刷新缓冲区

            in = new DataInputStream(new BufferedInputStream(new FileInputStream(dataFile)));
            double price = 0;
            int unit;
            String desc;
            double total = 0.0;
            try {
                while (true) {
                    price = in.readDouble();
                    unit  = in.readInt();
                    desc  = in.readUTF();
                    System.out.println(String.format("You ordered %d" + " units of %s at $%.2f%n", unit, desc, price));
                    total += unit * price;
                }
            } catch (EOFException e) {
            }
        } finally {
            if (out != null) {
                out.close();
            }
            if (in != null) {
                in.close();
            }
        }
    }
}

        注意,DataStreams 通过捕获 EOFException 来检测文件的结束条件,而不是测试无效的返回值。DataInput 方法的所有实现都使用 EOFException 而不是返回值。

        DataStreams 中的每个写入都有相应的专门化读取进行完全匹配。上述示例中,DataStreams 使用浮点数来表示货币的值,不过浮点数并不擅长用来表示精确的值,尤其是十进制分数,因为有些常见的值(比如 0.1)没有二进制的表示。所以,用于货币值的正确类型是 java.math.BigDecimal。但是,BigDecimal 是一种对象类型,因此它不能用于数据流。不过,BigDecimal 可以与对象流一起工作。// 数据流的局限性是它只支持基本数据类型和字符串类型

6、Java Basic I/O 中的对象流:Object Streams

        数据流支持基本数据类型的 I/O,对象流用来支持对象的 I/O。大多数实现 Serializable 接口的标准类都支持对其对象的序列化。// 把对象转为二进制

        对象流的类为 ObjectInputStream 和 ObjectOutputStream。这些类实现了 ObjectInput 和 ObjectOutput 接口,ObjectInput 和 ObjectOutput 是 DataInput 和 DataOutput 的子接口。所以数据流中的所有基本数据的 I/O 方法也会在对象流中实现。因此,对象流既可以操作基本数据类型也可以操作对象类型的数据

        对象流的示例代码如下:

public class ObjectStreams {

    static final String dataFile = "F:\\invoicedata.txt";

    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]);
            }
            out.flush();
        } 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();
        }
    }
}

        ObjectStreams 创建了与 DataStreams 相同的应用程序,只是做了一些更改。首先,价格现在是 BigDecimal 对象,以便更好地表示分数值。其次,将 Calendar 对象写入了数据文件,用来标注日期。

对复杂对象的输入和输出

        writeObject() 和 readObject() 方法使用起来很简单,但它们也包含一些非常复杂的对象管理逻辑。对于像 Calendar 这样的类,因为它只是封装了原始值,所以并不重要。但是许多对象还包含了对其他对象的引用。如果 readObject() 要从流中重构对象,它就必须能够重构该对象引用的所有对象,这些附加的对象又可能有自己的引用。所以,在这种情况下,writeObject() 需要遍历整个对象的引用网络,并将该网络中的所有对象都写入流。因此,writeObject() 的一次调用可能导致将大量对象写入流。// writeObject() 会写入对象中的对象

        如下图所示,调用 writeObject() 来写入名为 a 的单个对象。该对象包含对对象 b 和 c 的引用,而 b 包含对对象 d 和 e 的引用。所以调用 writeObject(a) 不仅写入 a,而且会写入重构 a 所需的所有对象,因此关系网中的其他四个对象也会被写入。当 readObject() 读回 a 时,其他四个对象也会被读回,并且所有原始对象的引用也都会被保留。 

        那么如果同一个流上的两个对象都包含对单例对象的引用,会发生什么呢?当它们被回读时,它们是否都指向同一个对象呢?答案是肯定的。一个流只能包含一个对象的一个副本,但它可以包含任意数量对该对象的引用。因此,如果显式地将一个对象写入同一个流两次,实际上只写入了两次引用。例如,如果下面的代码将 ob 对象两次写入流:

Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);

        每个 writeObject() 都必须与一个 readObject() 相匹配,所以读取流的代码看起来像这样:

Object ob1 = in.readObject();
Object ob2 = in.readObject();

        这将产生两个变量 ob1 和 ob2,但它们是对同一个对象的引用。

        不过,如果一个对象被写入两个不同的流,那么读取两个流时将会拥有两个不同的对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

swadian2008

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值