美文网首页
IO(二):Reader,Writer和PrintWriter

IO(二):Reader,Writer和PrintWriter

作者: JBryan | 来源:发表于2020-03-06 12:21 被阅读0次

    1.读取classpath资源

    我们知道,Java存放.class的目录或jar包也可以包含任意其他类型的文件,例如:
    配置文件,例如.properties;
    图片文件,例如.jpg;
    文本文件,例如.txt,.csv;
    ……
    从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties文件放到classpath中,就不用关心它的实际存放路径。
    在classpath中的资源文件,路径总是以/开头,我们先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件:

    try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
        // TODO:
    }
    

    如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:

    Properties props = new Properties();
    props.load(inputStreamFromClassPath("/default.properties"));
    props.load(inputStreamFromFile("./conf.properties"));
    

    这样读取配置文件,应用程序启动就更加灵活。

    2.序列化

    序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
    有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
    一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

    public interface Serializable {
    }
    
    2.1.序列化

    把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流:

    public static void main(String[] args) throws IOException {
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            try(ObjectOutputStream outputStream = new ObjectOutputStream(stream)){
                outputStream.write(123);
                outputStream.writeUTF("Hello");
                outputStream.writeObject(Double.valueOf(123.456));
            }
            System.out.println(Arrays.toString(stream.toByteArray()));
        }
    

    ObjectOutputStream既可以写入基本类型,如int,boolean,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object。
    因为写入Object时需要大量的类型信息,所以写入的内容很大。

    2.2.反序列化

    和ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:

    try (ObjectInputStream input = new ObjectInputStream(...)) {
        int n = input.readInt();
        String s = input.readUTF();
        Double d = (Double) input.readObject();
    }
    

    除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。
    readObject()可能抛出的异常有:
    ClassNotFoundException:没有找到对应的Class;
    InvalidClassException:Class不匹配。
    实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。

    3.Reader

    Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取:


    reader.jpg
    3.1.FileReader

    FileReader是Reader的一个子类,它可以打开文件并获取Reader。下面的代码演示了如何完整地读取一个FileReader的所有字符:

    public static void main(String[] args) throws IOException {
            Reader reader = new FileReader("C:\\upload_file\\IOTest.txt");
            while(true){
                int n = reader.read();
                if(n == -1){
                    break;
                }
                System.out.println((char) n);
            }
            reader.close();
        }
    

    我们可以先设置一个缓冲区,然后,每次尽可能地填充缓冲区:

    public void readFile() throws IOException {
        try (Reader reader = new FileReader("src/readme.txt")) {
            char[] buffer = new char[1000];
            int n;
            while ((n = reader.read(buffer)) != -1) {
                System.out.println("read " + n + " chars.");
            }
        }
    }
    
    3.2.InputStreamReader

    Reader和InputStream有什么关系?
    除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream。
    既然Reader本质上是一个基于InputStream的byte到char的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。示例代码如下:

    // 持有InputStream:
    InputStream input = new FileInputStream("src/readme.txt");
    // 变换为Reader:
    Reader reader = new InputStreamReader(input, "UTF-8");
    

    使用InputStreamReader,可以把一个InputStream转换成一个Reader。

    4.Writer

    Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
    Writer和OutputStream的区别如下:


    writer.jpg

    Writer是所有字符输出流的超类,它提供的方法主要有:
    写入一个字符(0~65535):void write(int c);
    写入字符数组的所有字符:void write(char[] c);
    写入String表示的所有字符:void write(String s)。

    4.1.FileWriter

    FileWriter就是向文件中写入字符流的Writer。它的使用方法和FileReader类似:

    try (Writer writer = new FileWriter("readme.txt")) {
        writer.write('H'); // 写入单个字符
        writer.write("Hello".toCharArray()); // 写入char[]
        writer.write("Hello"); // 写入String
    }
    
    4.2.OutputStreamWriter

    普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:

    try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
        // TODO:
    }
    

    上述代码实际上就是FileWriter的一种实现方式。这和上一节的InputStreamReader是一样的。

    5.PrintStream和PrintWriter

    PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:
    写入int:print(int)
    写入boolean:print(boolean)
    写入String:print(String)
    写入Object:print(Object),实际上相当于print(object.toString())

    以及对应的一组println()方法,它会自动加上换行符。
    PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException。

    5.1.PrintWriter

    PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。两者的使用方法几乎是一模一样的:

    public static void main(String[] args) throws IOException {
            StringWriter writer = new StringWriter();
            try(PrintWriter printWriter = new PrintWriter(writer)){
                printWriter.println("hello");
                printWriter.println(123);
                printWriter.println(true);
            }
            System.out.println(writer.toString());
        }
    

    相关文章

      网友评论

          本文标题:IO(二):Reader,Writer和PrintWriter

          本文链接:https://www.haomeiwen.com/subject/trbvrhtx.html