Java I/O

1. 文本I/O和二进制I/O

核心:二进制I/O不涉及编码和解码,因此比文本I/O更为高效。

计算机并不会区分文本文件和二进制文件。所有的文件都是通过二进制形式进行存储的。因此从本质上讲所有的文件都是二进制文件。文本I/O建立在二进制I/O的基础上,它提供了一层抽象,用于字符层次的编码与解码。在写入一个字符时,Java虚拟机会将统一码转化为文件的指定编码,而在读取字符时,将文件的指定编码转化为统一码。
我们知道,Windows中文本文件的默认编码方案是ASCII码。从下图可以看出,当用文本I/O程序将字符串‘199’写入文件,那么每个字符都会写入到文件中。比如字符‘1’的统一码为0x0031(十六进制),正好对应十进制的49,因此字符‘1’的ASCII码是49。
这里写图片描述
二进制I/O不需要转化。如果使用二进制I/O程序向文件写入一个数值,就是将内存中的那个值直接复制到文件中。
这里写图片描述
一般来说,对于文本编辑器或文本输出程序创建的文件,应该使用文本输入来读取。对于Java二进制输出程序创建的文件,应该使用二进制输入来读取。

使用二进制I/O的优点:

(1). 二进制I/O不需要编码和解码,所以它比文本文件效率要高。
(2). 二进制文件与主机的编码方案无关,因此它是可移植的。

2. 二进制I/O类

下图列出了一些实现二进制I/O的类。其中InputStream类是二进制输入类的根类,而OutputStream类是二进制输出类的根类。
这里写图片描述
下图是抽象的InputStream类定义字节输入流的方法
这里写图片描述
下图是抽象的OutputStream类定义字节输出流的方法
这里写图片描述

2.1 FileInputStream和FileOutputStream

FileInputStream类用于从文件中读取字节。
FileOutputStream类用于向文件中写入字节。
这两个类的所有方法都是从InputStream类和OutputStream类继承过来的,它们本身并没有引入新的方法。
FileInputStream类的构造方法如下图所示:
这里写图片描述
如果试图为一个不存在的文件创建FileInputStream对象,将会发生java.io.FileNotFoundException。

下图展示了构建FileOutputStream对象的构造方法:
如果文件不存在,就会创建一个新文件。如果文件已经存在,前两个方法会删除文件的当前内容。如果想保留文件现有内容的同时并且向文件追加新数据,可以将最后两个构造方法的参数append设置为true。
这里写图片描述
几乎所有的I/O类中的方法都会抛出异常java.io.IOException。因此,必须要在方法中声明会抛出java.io.IOException异常,或是将代码放到try-catch块中。
下面一段代码展示这两个类的用法:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class TestFileStream {
    public static void main(String[] args) throws IOException{
        try(
                FileOutputStream output = new FileOutputStream("temp.dat");
                ){
            for(int i = 1; i <= 10; i++)
                output.write(i);
        }

        try(
                FileInputStream input = new FileInputStream("temp.dat");
                ){
            int value;
            while ((value = input.read()) != -1)
                System.out.print(value + " ");
        }
    }
}

程序使用了try-with-resources来声明和创建输入输出流,从而在使用后可以自动关闭流。InputStream和OutputStream实现了AutoClosable接口。AutoClosable接口定义了close()方法来关闭资源,任何AutoClosable类型的对象都可以用于try-with-resources语法中,实现自动关闭。
在该程序中,调用write(i)方法和调用write((byte)i)方法具有相同功能。

注意:
(1) 当流不再需要使用时,记得使用close()方法将其关闭,或者使用try-with-resource语句自动关闭。不关闭流可能会在输出文件中造成数据受损,或者导致其它的程序设计错误。
(2) FileInputStream类的实例可以作为参数去构造一个Scanner对象,而FileOutputStream类的实例可以作为参数去构造一个PrintWriter对象。可以创建一个PrintWriter对象来向文件中追加文本。如果temp.txt文件不存在,就创建该文件,如果该文件已经存在,就将新数据追加到该文件中。
new Scanner(new FileInputStream(“temp.txt”));
new PrintWriter(new FileOutputStream(“temp.txt”, true));

2.2 FilterInputStream和FilterOutputStream

过滤数据流(filter stream)是为某种目的过滤字节的数据流。基本字节输入流提供的读取方法read只能用来读取字节。如果要读取整数值、双精度值或字符串,那就需要一个过滤器类来包装字节输入流。使用过滤器类就可以读取整数值、双精度值和字符串,而不是字节或字符。FilterInputStream类和FilterOutputStream类是过滤数据的基类。需要处理基本数值类型时,就使用DataInputStream类和DataOutputStream类来过滤字节。

2.3 DataInputStream和DataOutputStream

DataInputStream从数据流读取字节,并将它们转化为合适的基本类型或字符串。DataInputStream类扩展FilterInputStream类,并实现DataInput接口。
这里写图片描述
DataOutputStream将基本类型的值或字符串转化为字节,并将字节输出到数据流。DataOutputStream类扩展FilterOutputStream类,并实现DataOutput接口。
这里写图片描述
关于二进制I/O中的字符与字符串
一个统一码由两个字节组成。writeChar(char c)方法将字符c的统一码写入输出流。

writeChars(String s)方法将字符串s中所有字符的统一码写到输出流中。

writeBytes(String s)方法将字符串s的每个字符统一码的低字节写到输出流,统一码的高字节被丢弃。writeBytes方法适用于由ASCII码字符构成的字符串,因为ASCII码仅存储统一码的低字节。如果一个字符串包含非ASCII码的字符,必须使用writeChars方法实现写入这个字符串。

writeUTF(String s)方法将两个字节的长度信息写入输出流,后面紧跟的是字符串s中每个字符的改进版UTF-8的形式。UTF-8是一种编码方案,它允许系统可以同时操作统一码及ASCII码。大多数操作系统使用ASCII码,Java使用统一码。ASCII码字符集是统一码字符集的子集。由于许多应用程序只需要ASCII码字符集,所以将8位的ASCII码字符集表示为16位的统一码是很浪费的。UTF-8的修改版方案分别使用1字节、2字节或3字节来存储字符。如果字符的编码值小于或等于0x7F就将该字符编码为一个字节,如果字符的编码大于0x7F而小于或等于0x7FF就将该字符编码为两个字节,如果该字符的编码值大于0x7FF就将该字符编码为三个字节。

UTF-8字符起始的几位表明这个字符是存储在一个字节、两个字节还是三个字节中。如果首位是0,那它就是一个字节的字符。如果前三位是110,那它就是两个字节序列的第一个字节。如果前四位是1110,那它就是三个字节序列的第一个字节。UTF-8字符之前的两个字节用来存储表明字符串的字符个数的信息。例如,writeUTF(“ABCDEF”)写入文件的8个字节(即 00 06 41 42 43 44 45 46),因此头两个字节存储的是字符串中的字符个数。

UTF-8格式具有存储每个ASCII码就节省一个字节的优势,因为一个统一码的存储需要两个字节,而在UTF-8格式中ASCII字符仅占一个字节。如果一个长字符串的大多数字符都是普通的ASCII字符,采用UTF-8格式存储更加高效。
下面通过一段代码展示这两个类的使用:

import java.io.*;

public class TestDataStream {
    public static void main(String[] args) throws IOException {
        try (
                DataOutputStream output = new DataOutputStream(new FileOutputStream("temp.dat"));
                ){
            output.writeUTF("John");
            output.writeDouble(85.5);
            output.writeUTF("Jim");
            output.writeDouble(185.5);
            output.writeUTF("George");
            output.writeDouble(105.25);
        }

        try (
                DataInputStream input = new DataInputStream(new FileInputStream("temp.dat"));
                ){
            System.out.println(input.readUTF() + " " + input.readDouble());
            System.out.println(input.readUTF() + " " + input.readDouble());
            System.out.println(input.readUTF() + " " + input.readDouble());
        }
    }
}

从上面的程序可以看出,应该按存储的顺序和格式读取文件中的数据。比如学生的姓名是用writeUTF方法以UTF-8格式写入的,所以,读取时必须使用readUTF方法。
上面程序可以看作工作在一个管道线中。
这里写图片描述
检查文件的末尾
如果到达InputStream的末尾之后还继续从中读取数据,就会发生EOFException异常。这个异常可以用来检查是否已经到达文件末尾:

import java.io.*;

public class DetectEndOfFile {
    public static void main(String[] args) {
        try {
            try (
                    DataOutputStream output = new DataOutputStream(new FileOutputStream("test.dat"));
                    ) {
                output.writeDouble(4.5);
                output.writeDouble(2.5);
                output.writeDouble(3.2);
            }

            try (
                    DataInputStream input = new DataInputStream(new FileInputStream("test.dat"));
                    ) {
                while (true)
                    System.out.println(input.readDouble());
            }
        }
        catch (EOFException e) {
            System.out.println("All data were read");
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.4 BufferedInputStream和BufferedOutputStream

BufferedInputStream类和BufferedOutputStream类可以通过减少磁盘的读写次数来提高输入和输出的速度。使用BufferedInputStream时,磁盘上的整块数据一次性地读入到内存中的缓冲区中,然后从缓冲区中将个别的数据传递到程序中。使用BufferedOutputStream,个别的数据首先写入到内存中的缓冲区中。当缓冲区已满时,缓冲区的所有数据一次性写入到磁盘中。具体过程如下图所示:
这里写图片描述
BufferedInputStream类和BufferedOutputStream类没有包含新的方法。BufferedInputStream类和BufferedOutputStream类中的所有方法都是从InputStream类和OutputStrean类继承而来的。BufferedInputStream类和BufferedOutputStream类在后台管理一个缓冲区,根据要求自动从磁盘中读取数据和写入数据。
如下图所示,BufferedInputStream类和BufferedOutputStream类的第二个构造方法指定了缓冲区大小,如果没有指定,默认是512个字节。
这里写图片描述
这里写图片描述
注意
应该总是使用缓冲区I/O来加速输入和输出,比如前面的程序可以作如下改变:
DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(“temp.dat”)));
DataInputStream input = new DataInputStream(new BufferedInputStream(new FileInputStream(“temp.dat”)));
对于小文件,我们可能注意不到性能的提升。但是,对于超过100M的大文件,我们将会看到使用缓冲区的I/O带来的实质性的性能提升。

2.5 ObjectInputStream和ObjectOutputStream

要点:
ObjectInputStream类和ObjectOutputStream类可用于读/写可序列化对象

DataInputStream类和DataOutputStream类可以实现基本数据类型与字符串的输入和输出。而ObjectInputStream类和ObjectOutputStream类除了可以实现基本类型与字符串的输入和输出以外,还可以实现对象的输入和输出。由于ObjectInputStream类和ObjectOutputStream类包含DataInputStream类和DataOutputStream类的所有功能,所有完全可以用ObjectInputStream类和ObjectOutputStream类代替DataInputStream类和DataOutputStream类

ObjectInputStream扩展InputStream类,并实现接口ObjectInput和ObjectStreamConstants。其中,ObjectInput是DataInput的子接口,ObjectStreamConstants包含支持ObjectInputStream类和ObjectOutputStream类所用的常量。
这里写图片描述
ObjectOutputStream扩展OutputStream类,并实现接口ObjectOutput和ObjectStreamConstants,ObjectOutput是DataOutput的子接口。
这里写图片描述
下面通过一个简单程序展示这两个类的用法:

import java.io.*;
import java.util.Date;

public class TestObjectStream {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        try (
                ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("object.dat"));
                ) {
            output.writeUTF("John");
            output.writeDouble(85.5);
            output.writeObject(new Date());
        }

        try (
                ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.dat"));
                ) {
            String name = input.readUTF();
            double score = input.readDouble();
            Date date = (Date)(input.readObject());
            System.out.println(name + " " + score + " " + date);
        }
    }
}

其实程序可以在流中添加一个缓冲区:

可以向数据流中写入多个对象或基本类型数据。从对应的ObjectInputStream中读回这些对象时,必须与其写入时的类型和顺序相同

readObject()方法可能会抛出java.lang.ClassNotFoundException异常,这是个必检异常。

Serializable接口

并不是每一个对象都可以写到输出流。可以写入输出流中的对象称为可序列化的(serializable)。因为可序列化的对象是java.io.Serializable接口的实例,所以,可序列化对象的类必须实现Serializable接口。

Serializable接口是一种标记接口(marker interface),因为它没有方法,所以不需要在类中为实现Serializable接口增加额外的代码。实现这个接口可以启动Java的序列化机制,自动完成存储对象和数组的过程。

为了体会这个自动功能和理解对象是如何存储的,考虑一下不使用这一功能,存储一个对象需要做哪些工作。假使要存储一个ArrayList对象。为了完成这个任务,需要存储列表中的每个元素。每个元素是一个可能包含其他对象的对象,显然这是一个非常繁杂的过程。辛运的是,不必手工完成这个过程。Java提供一个内在机制自动完成写对象的过程。这个过程称为对象序列化(object serialization),它是在ObjectOutputStream中实现的。与此相反,读取对象的过程称作对象反序列化(object deserialization),它是在ObjectInputStream类中实现的。

许多Java API中的类都实现了Serializable接口。所有针对基本类型值得包装类,java.math.BigInteger、java.math.BigDecimal、java.lang.String、java.lang.StringBuilder、java.lang.StringBuffer、java.util.Date以及java.util.ArrayList都实现了java.io.Serializable接口。试图存储一个不支持Serializable接口的对象会引起一个NotSerializableException异常。

当存储一个可序列化对象时,会对该对象的类进行编码。编码包括类名、类的签名、对象实例变量的值以及该对象引用的任何其他对象的闭包,但是不存储对象静态变量的值
注意:非序列化的数据域
如果一个对象是Serializable的实例,但它包含了非序列化的实例数据域,那么就不可以序列化这个对象了。为了使该对象是可序列化的,需要给这个数据域加上关键字transient,告诉Java虚拟机将对象写入对象流时忽略这些数据域。

public class C implements java.io.Serializable {
    private int v1;
    private static double v2;
    private transient A v3 = new A();
}

class A {} // A is not serializable

当C类的一个对象进行序列化时,只需要序列化变量v1。因为v2是一个静态变量,所以没有序列化。因为v3标记为transient,所以也没有序列化。如果v3没有标记为transient,将会发生异常java.io.NotSerializableException。
注意
如果一个对象不止一次写入对象流,系统不会存储对象的多个副本。第一次写入一个对象时,就会为它创建一个序列号。Java虚拟机将对象的所有内容和序列一起写入对象流。以后每次存储时,如果在写入相同的对象时,就之存储序列号。读取这些对象时,它们的引用相同,因为在内存中实际上存储的只是一个对象。

下面通过一个序列化数组的程序来展示序列化过程。如果数组中的所有元素都是可序列化的,这个数组就是可序列化的。一个完整的数组可以用writeObject方法存入到文件,随后用readObject方法恢复:

import java.io.*;

public class TestObjectStreamForArray {
    public static void main(String[] args) throws IOException, ClassNotFoundException{
        int[] numbers = {1, 2, 3, 4, 5};
        String[] strings = {"John", "Susan", "Kim"};

        try (
                ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("array.dat", true));
                ) {
            output.writeObject(numbers);
            output.writeObject(strings);
        }

        try (
                ObjectInputStream input = new ObjectInputStream(new FileInputStream("array.dat"));
                ) {
            int[] newNumbers = (int[])(input.readObject());
            String[] newStrings = (String[])(input.readObject());

            for(int i = 0; i < newNumbers.length; i++)
                System.out.print(newNumbers[i] + " ");
            System.out.println();

            for(int i = 0; i < newStrings.length; i++)
                System.out.print(newStrings[i] + " ");
        }
    }
}

2.6 RandomAccessFile

要点
Java提供了RandomAccessFile类,允许从文件的任何位置进行数据的读写。

前面所使用的流都是只读的或只写的,这些流称为顺序(sequential)流。使用顺序流打开的文件称为顺序访问文件。顺序访问文件的内容不能更新。然而,经常需要修改文件。Java提供了RandomAccessFile类,允许在文件的任意位置上读写。使用RandomAccessFile类打开的文件称为随机访问文件。

RandomAccessFile类实现了DataInput和DataOutput接口。
这里写图片描述
当创建一个RandomAccessFile时,可以指定两种模式(“r”或”rw”)之一,例如,下面的语句创建一个新的数据流raf,它允许程序对文件test.dat进行读取或写入:

RandomAccessFile raf = new RandomAccessFile("test.dat", "rw");

如果文件test.dat已经存在,则创建raf以便访问这个文件;如果test.dat不存在,则创建一个名为test.dat的新文件,再创建raf来访问这个新文件。raf.length()方法返回在给定时刻文件test.dat中的字节数。如果向文件中追加新数据,raf.length()就会增加。
注意
如果不想改动文件,就将文件以”r”模式打开。这样做可以防止不经意中改动文件。

随机访问文件是由字节序列组成的。一个称为文件指针(file pointer)的特殊标记定位这些字节中的某个字节的位置。文件的读写操作就是在文件指针所指的位置上进行的。打开文件时,文件指针置于位置的起始位置。在文件中进行读写数据后,文件指针就会向前移到下一个数据项。例如,如果使用readInt()方法读取一个int数据,Java虚拟机就会从文件指针处读取4个字节,这样,文件指针就会从它之前的位置向前移动4个字节。
这里写图片描述
设raf是RandomAccessFile的一个对象,可以调用raf.seek(position)方法将文件指针移到指定的位置。raf.seek(0)方法将文件指针移到文件的起始位置,而raf.seek(raf.length())方法则将文件的指针移到文件的末尾。下面通过一个程序来展示:

import java.io.IOException;
import java.io.RandomAccessFile;

public class TestRandomAccessFile {
    public static void main(String[] args) throws IOException{
        try (
                RandomAccessFile inout = new RandomAccessFile("input.dat", "rw");
                ) {
            // Clear the file to destroy the old contents if exists
            inout.setLength(0);

            for(int i = 0; i < 200; i++)
                inout.writeInt(i);

            System.out.println("Current file length is " + inout.length());

            //Retrieve the first number
            inout.seek(0);
            System.out.println("The first number is " + inout.readInt());

            //Retrieve the second number
            inout.seek(1 * 4);
            System.out.println("The second number is " + inout.readInt());

            //Retrieve the tenth number
            inout.seek(9 * 4);
            System.out.println("The tenth number is " + inout.readInt());

            //Modify the eleventh number
            inout.writeInt(555);

            //Append a new number
            inout.seek(inout.length()); //Move the file pointer to the end
            inout.writeInt(999);

            System.out.println("The new length is " + inout.length());

            //Retrieve the new eleventh number
            inout.seek(10 * 4);
            System.out.println("The eleventh number is " + inout.readInt());
        }
    }
}

运行结果如下:
这里写图片描述

2.7 ByteArrayInputStream和ByteArrayOutputStream

ByteArrayInputStream类
ByteArrayInputStream使用字节数组作为源的输入流的一个实现。这个类有两个构造函数:

ByteArrayInputStream(byte[] buf) 
ByteArrayInputStream(byte[] buf, int offset, int length)

在此,buf是输入源。第二个构造函数从字节数组的子集创建InputStream对象,这个数组子集从offset指定的索引位置的字符开始,共length个字符。

close()方法对ByteArrayInputStream对象没有效果。所以不需要为ByteArrayInputStream对象调用close()方法。但如果那样做也不会产生错误。

ByteArrayInputStream实现了mark()和reset()方法。然而,如果没有调用mark()方法,那么reset()方法会将流指针设置为流的开头,这样相当于设置为传递给构造函数的字节数组的开头。下面一个例子显示如何使用reset()方法来读取相同的输入两次。在这个例子中,程序以小写形式读取并打印字母“abc”一次,然后再次使用大写形式读取并打印。

import java.io.ByteArrayInputStream;

public class ByteArrayInputStreamDemo {
    public static void main(String[] args) {
        String tmp = "abc";
        byte[] b = tmp.getBytes();
        ByteArrayInputStream in = new ByteArrayInputStream(b);

        for (int i = 0; i < 2; i++) {
            int c;
            while ((c = in.read()) != -1) {
                if (i == 0) {
                    System.out.print((char)c);
                }else {
                    System.out.print(Character.toUpperCase((char)c));
                }
            }
            System.out.println();
            in.reset();
        }
    }
}

输出:
abc
ABC

ByteArrayOutputStream类
ByteArrayOutputStream使用字节数组作为目标的输出流的一个实现。ByteArrayOutputStream有两个构造函数:

ByteArrayOutputStream() 
ByteArrayOutputStream(int size)

第一个构造函数创建了一个32字节的缓冲区。第二个构造函数创建一个由size指定大小的缓冲区。缓冲区被保存在ByteArrayOutputStream中受保护的buf域变量中。如果需要的话,缓冲区的大小会自动增加。缓冲区能够保存的字节数量包含在ByteArrayOutputStream中受保护的count域变量中。

close()方法对ByteArrayOutputStream对象没有效果。所以不需要为ByteArrayOutputStream对象调用close()方法。但如果那样做也不会产生错误。

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteArrayOutputStreamDemo {
    public static void main(String[] args) {
        ByteArrayOutputStream f = new ByteArrayOutputStream();
        String s = "This should end up in the array";
        byte[] buf = s.getBytes();

        try {
            f.write(buf);
        }
        catch (IOException e) {
            System.out.println("Error Writing to Buffer");
            return;
        }

        System.out.println("Buffer as a string");
        System.out.println(f.toString()); // 使用平台默认的字符集,通过解码字节将缓冲区内容转换为字符串
        System.out.println("Into array");
        byte[] b = f.toByteArray();
        for (int i = 0; i < b.length; i++)
            System.out.print((char)b[i]);

        System.out.println("\nTo an OutputStream()");

        try (
                FileOutputStream f2 = new FileOutputStream("test.txt"); 
                ) {
            f.writeTo(f2); // 将此 byte 数组输出流的全部内容写入到指定的输出流参数中
        }
        catch (IOException e) {
            System.out.println("I/O Error: " + e);
            return;
        }

        System.out.println("Doing a reset");
        f.reset();

        for (int i = 0; i < 3; i++)
            f.write('X');

        System.out.println(f.toString());
    }
}

会在test.txt文件中写入“This should end up in the array”字符串,并且在控制台输出:
Buffer as a string
This should end up in the array
Into array
This should end up in the array
To an OutputStream()
Doing a reset
XXX

3. 字符流

虽然字节流为处理各种类型的I/O操作提供充足的功能,但是它们不能直接操作Unicode字符。因为Java的一个主要目的就是实现代码的“一次编写,到处运行”,所以需要为字符提供直接的I/O支持。其中,Reader和Writer抽象类位于字符流层次的顶部。

3.1 Reader和Writer

Reader类
Reader是抽象类,定义了Java的流字符输入模型。该类实现了AutoCloseable、Closeable以及Readable接口。下表描述了Reader类中的方法。
这里写图片描述
这里写图片描述

Writer类
Writer是定义流字符输出模型的抽象类。该类实现了AutoCloseable、Closeable、Flushable和Appendable接口。下表描述了Writer类中的方法。
这里写图片描述
这里写图片描述

3.2 FileReader和FileWriter

FileReader类

FileReader类可以创建用于读取文件内容的Reader对象,常用的两个构造为:

FileReader(String filePath)
FileReader(File fileObj)

下面的例子显示如何从文件中读取文本行,以及如何在标准输出设备上进行读取自己的源文件,源文件必须在当前目录下。

import java.io.FileReader;
import java.io.IOException;

public class FileReaderDemo {
    public static void main(String[] args) throws IOException{
        try (
                FileReader fr = new FileReader("FileReaderDemo.java")) {
            int c;

            while ((c = fr.read()) != -1)
                System.out.print((char)c);
        }
    }
}

FileWriter类
FileWriter类可以创建能够用于写入文件的Writer对象,该类最常用的4个构造方法如下:

FileWriter(String filePath)
FileWriter(String filePath, boolean append)
FileWriter(File fileObj)
FileWriter(File fileObj, boolean append)

FileWriter对象的创建不依赖于已经存在的文件。当创建对象时,FileWriter会在打开文件之前为输出创建文件。对于这种情况,如果试图打开只读文件,就会抛出IOException。
下面通过一个程序演示该类的用法:

import java.io.FileWriter;
import java.io.IOException;

public class FileWriterDemo {
    public static void main(String[] args) throws IOException {
        String source = "Now is the time for all good men\n"
                + " to come to the aid of their country\n"
                + " and pay their due taxes.";
        char[] buffer = new char[source.length()];
        source.getChars(0, source.length(), buffer, 0);
        System.out.println(source.length());

        try (
                FileWriter f1 = new FileWriter("file1.txt");
                FileWriter f2 = new FileWriter("file2.txt");
                FileWriter f3 = new FileWriter("file3.txt");
                ) {
            // write to first file
            for (int i = 0; i < buffer.length; i += 2) {
                f1.write(buffer[i]);
            }

            // write to second file
            f2.write(buffer);

            // write to third file
            f3.write(buffer, buffer.length-buffer.length/4, buffer.length/4);
        }
    }
}

3.3 PrintWriter

PrintWriter类可以用来创建一个文件并向文件写入数据。首先,必须为一个文本文件创建一个PrintWriter对象,如下所示:

PrintWriter output = new PrintWriter(filename);

然后,可以调用PrintWriter对象上的print、println和printf方法向文件写入数据。下表总结了PrintWriter中的常用方法。
这里写图片描述
下面用一个程序展示该类的用法:
注意:
必须使用close()方法关闭文件。如果没有该方法,数据就不能正确地保存在文件中。

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;

public class PrintWriterDemo {
    public static void main(String[] args) throws IOException {
        File file = new File("ages.txt");
        if (file.exists()) {
            System.out.println("File already exists");
            System.exit(1);
        }
        PrintWriter output = new PrintWriter(file);

        output.print("Andrew Ng ");
        output.println(42);
        output.print("Geoffrey Hinton ");
        output.println(71);

        output.close();
    }
}

3.4 CharArrayReader和CharArrayWriter

CharArrayReader类
CharArrayReader类是使用字符数组作为源的一个输入流实现。该类具有两个构造函数,每个构造函数都需要一个字符数组来提供数据源:

CharArrayReader(char[] array)
CharArrayReader(char[] array, int start, int numChars)

其中,array是输入源。第2个构造函数根据字符数组的子集创建Reader对象,该子集从start指定的索引位置的字符开始,共numChars个字符。
CharArrayReader实现的close()方法不会抛出任何异常,因为这不可能失败。

import java.io.CharArrayReader;
import java.io.IOException;

public class CharArrayReaderDemo {
    public static void main(String[] args) {
        String tmp = "abcdefghijklmnopqrstuvwxyz";
        int length = tmp.length();
        char[] c = new char[length];

        tmp.getChars(0, length, c, 0);
        int i;

        try(
                CharArrayReader input1 = new CharArrayReader(c);
                ) {
            System.out.println("input1 is: ");
            while ((i = input1.read()) != -1) {
                System.out.print((char)i);
            }
            System.out.println();
        }
        catch (IOException e) {
            System.out.println("I/O Error: " + e);
        }

        try (
                CharArrayReader input2 = new CharArrayReader(c, 0, 5)
                ) {
            System.out.println("input2 is: ");
            while ((i = input2.read()) != -1) {
                System.out.print((char)i);
            }
            System.out.println();
        }
        catch (IOException e) {
            System.out.println("I/O Error: " + e);
        }
    }
}

CharArrayWriter类
CharArrayWriter类是将字符数组写入到输出流中。CharArrayWriter类具有两个构造函数,如下所示:

CharArrayWriter()
CharArrayWriter(int numChars)

第一个构造方法使用了默认大小(默认为32)的缓冲区。第二个构造方法创建由numChars指定大小的缓冲区。缓冲区保存在CharArrayWriter类的buf域变量中。如果需要,缓冲区的大小可以自动增加。缓冲区能够容纳的字符数量保存在CharArrayWriter类的count域变量中。
close()方法对CharArrayWriter没有影响。
下面的例子重写前面演示ByteArrayOutputStream的例子,演示了CharArrayWriter类的使用。

import java.io.CharArrayWriter;
import java.io.FileWriter;
import java.io.IOException;

public class CharArrayWriterDemo {
    public static void main(String[] args) throws IOException{
        CharArrayWriter output = new CharArrayWriter();
        String s = "This should be end up in the array";
        char[] buf = new char[s.length()];

        s.getChars(0, s.length(), buf, 0);

        try {
            output.write(buf);
        }
        catch (IOException e) {
            System.out.println("Error Writing to Buffer");
            return;
        }

        System.out.println("Buffer as a string");
        System.out.println(output.toString());
        System.out.println("Into array");

        char[] c = output.toCharArray();
        for(int i = 0; i < c.length; i++) {
            System.out.print(c[i]);
        }

        System.out.println("\nTo a FileWriter()");

        try (
                FileWriter fw = new FileWriter("test.txt")
                ) {
            output.writeTo(fw);
        }
        catch (IOException e) {
            System.out.println("I/O Error: " + e);
        }

        System.out.println("Doing a reset");
        output.reset();

        for (int i = 0; i < 3; i++)
            output.write('X');

        System.out.println(output.toString());
    }
}

输出如下:
Buffer as a string
This should be end up in the array
Into array
This should be end up in the array
To a FileWriter()
Doing a reset
XXX

3.5 BufferedReader和BufferedWriter

BufferedReader
BufferedReader类通过缓冲输入提高性能,该类具有两个构造方法:

BufferedReader(Reader inputStream)
BufferedReader(Reader inputStream, int bufSize)

关闭BufferedReader对象也会导致inputStream 指定的底层流被关闭。
从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。

通常,Reader 所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。因此,建议用 BufferedReader 包装所有 read() 操作可能开销很高的 Reader(如 FileReader 和 InputStreamReader)。例如,
BufferedReader in = new BufferedReader(new FileReader(“in.txt”));
将缓冲指定文件的输入。如果没有缓冲,则每次调用 read() 或 readLine() 都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。

通过用合适的 BufferedReader 替代每个 DataInputStream,可以对将
DataInputStream 用于文字输入的程序进行本地化。 意思就是在读文件的时候,要是不使用缓冲的话,读一个就得转换成字符,效率低。(文件是二进制的数字哦基本单位就是字节了)

/读取一个文本行。通过下列字符之一即可认为某行已终止:换行 (‘\n’)、回车 (‘\r’) 或回车后直接跟着换行。
//返回:包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null
readLine():

字符读取流缓冲区:
该缓冲区提供了一个一次读一行的方法 readLine,方便于对文本数据的获取。
当返回null时,表示读到文件末尾
注意:
readLine方法返回的时候只返回回车符之前的数据内容。并不返回回车符。所有再把数据写出去的时候,还得把整个换行符给补上去哟。

通常, Reader 所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。因此,建议用 BufferedReader 包装所有其 read() 操作可能开销很高的 Reader (如 FileReader 和 InputStreamReader)。

import java.io.*;

public class BufferedReaderDemo {
    public static void main(String[] args) {
         try (
                 BufferedReader bufr = new BufferedReader(new FileReader("file1.txt"))
         ) {
             String line;
             while ((line = bufr.readLine()) != null) {
                 System.out.println(line);
             }
         }
         catch (IOException e) {
            e.printStackTrace();
         }
    }
}

BufferedWriter类
BufferedWriter是字符缓冲输出流,继承于Writer,作用是为其他字符输出流添加一些缓冲功能。其中的写入函数有下面三个:

void    write(char[] cbuf, int off, int len) // 写入字符数组的某一部分。
void    write(int c)                         // 写入单个字符。
void    write(String s, int off, int len)    // 写入字符串的某一部分。

对于第一个write()方法,写入的是字符数组cbuf从off起始位开始的len个字符,如果len数值大于(cbuf.length-off)数值,就会抛出java.lang.IndexOutOfBoundsException

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedWriterDemo {
    public static void main(String[] args) {
        char[] cbuf = {'H', 'e', 'l', 'l', 'o'};

        try (
                BufferedWriter out = new BufferedWriter(new FileWriter(new File("file1.txt")));
                ) {
            out1.write(cbuf, 0, 5);
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.6 InputStreamReader和OutputStreamWriter

要点:
一般在操作输入输出内容的时候就需要使用字节或字符流,但是有些时候需要将字符流变为字节流的形式,或者将字节流变为字符流的形式,所以,就需要另外一组转换流的操作类。

转换步骤:
如果以文件操作为例,则在内存中的字符数据需要通过OutputStreamWriter变为字节流才能保存在文件之中,读取的时候需要将读入的字节流通过InputStreamReader变为字符流
这里写图片描述

InputStreamReader类
InputStreamReader 将字节流转换为字符流。是字节流通向字符流的桥梁。如果不指定字符集编码,该解码过程将使用平台默认的字符编码,如:GBK。 为了达到最高效率,可要考虑在 BufferedReader 内包装 InputStreamReader。例如:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

常用的构造方法如下:

InputStreamReader(InputStream in); //构造一个默认编码集的InputStreamReader类
InputStreamReader(InputStream in, String charsetName); //构造一个指定编码集的InputStreamReader类
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class InputStreamReaderDemo {
    public static void main(String[] args) {
        try (
                InputStreamReader input = new InputStreamReader(new FileInputStream("file.txt"));
                ) {
            char[] cbuf = new char[1024];
            int len = input.read(cbuf); //将file.txt文件中的字符读到cbuf中
            System.out.println(new String(cbuf, 0, len));
            input.close();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

OutputStreamWriter类
OutputStreamWriter是字符流通向字节流的桥梁,可使用指定的 charset 将要写入流中的字符编码成字节。(即用字符流的方式写字节流)

OutputStreamWriter(OutputStream out) //创建使用默认字符编码的 OutputStreamWriter
OutputStreamWriter(OutputStream out, String charsetName) //创建使用指定字符集的 OutputStreamWriter
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class OutputStreamWriterDemo {
    public static void main(String[] args) {
        try (
                OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream("file.txt"))
                ) {
            out.write("Hello");
            out.close();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值