• Java里的IO基础知识笔记:IO流、字节流/字符流、File对象读取、输入流/输出流(使用过后及时关闭、缓冲区)、Filter模式、ZIP操作、读取classpath资源的意义、序列化/反序列化、Reader/Writer、使用Files工具类及其局限性


    一、IO流介绍

    1、IO是指Input/Output,即输入和输出。以内存为中心:

    • Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。

    • Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。

      为什么要把数据读到内存才能处理这些数据?因为代码是在内存中运行的,数据也必须读到内存,最终的表示方式无非是byte数组,字符串等,都必须存放在内存里。

    2、IO流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为IO流。

    3、字节流 —— InputStream/OutputStream

      IO流以byte(字节)为最小单位,因此也称为字节流。例如,我们要从磁盘读入一个文件,包含6个字节,就相当于读入了6个字节的数据:

    ╔════════════╗
    ║   Memory   ║
    ╚════════════╝
           ▲
           │0x48
           │0x65
           │0x6c
           │0x6c
           │0x6f
           │0x21
     ╔═══════════╗
     ║ Hard Disk ║
     ╚═══════════╝

      这6个字节是按顺序读入的,所以是输入字节流 —— InputStream。

      反过来,我们把6个字节从内存写入磁盘文件,就是输出字节流 —— OutputStream。

      在Java中,InputStream代表输入字节流,OuputStream代表输出字节流,这是最基本的两种IO流。

    4、字符流 —— Reader / Writer

      如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么按照char来读写显然更方便,这种流称为字符流

      Java提供了ReaderWriter表示字符流,字符流传输的最小数据单位是char

      我们把char[]数组Hi你好这4个字符用Writer字符流写入文件,并且使用UTF-8编码,得到的最终文件内容是8个字节,英文字符Hi各占一个字节,中文字符你好各占3个字节;反过来,我们用Reader读取以UTF-8编码的这8个字节,会从Reader中得到Hi你好这4个字符。

    5、因此,ReaderWriter本质上是一个能自动编解码的InputStreamOutputStream

      使用Reader,数据源虽然是字节,但我们读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了解码,转换成了char

      使用InputStream,我们读入的数据和原始二进制数据一模一样,是byte[]数组,但是我们可以自己把二进制byte[]数组按照某种编码转换为字符串。

      究竟使用Reader还是InputStream,要取决于具体的使用场景。如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更方便一些。

      WriterOutputStream是类似的。

    6、同步和异步IO:

      同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。

      而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。

    7、Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。上面我们讨论的InputStreamOutputStreamReaderWriter都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStreamFileOutputStreamFileReaderFileWriter

    8、小结:

      IO流是一种流式的数据输入/输出模型:

    • 二进制数据以byte为最小单位在InputStream/OutputStream中单向流动;

    • 字符数据以char为最小单位在Reader/Writer中单向流动。

      Java标准库的java.io包提供了同步IO功能:

    • 字节流接口:InputStream/OutputStream

    • 字符流接口:Reader/Writer

    二、File对象

    1、计算机系统中,文件是非常重要的存储方式。Java的标准库java.io提供了File对象来操作文件和目录。 要构造一个File对象,需要传入文件路径。

    2、需要注意的是:因为Windows和Linux的路径分隔符不同,所以File对象提供了一个静态变量用于表示当前平台的系统分隔符

    System.out.println(File.separator); 
    // 根据当前平台打印""或"/"
    // 使用提供的系统分隔符可以避免一些跨平台导致的错误,这个以前就遇到过

    3、Java标准库的java.io.File对象表示一个文件或者目录:

    • 创建File对象本身不涉及IO操作;
    • 可以获取路径/绝对路径/规范路径:getPath()/getAbsolutePath()/getCanonicalPath()
    • 可以获取目录的文件和子目录:list()/listFiles()
    • 可以创建或删除文件和目录:createNewFile()/delete()
    • File对象获取到一个文件时,还可以进一步判断文件的权限和大小:boolean canRead():是否可读;boolean canWrite():是否可写;boolean canExecute():是否可执行;long length():文件字节大小
    • 调用isFile(),判断该File对象是否是一个已存在的文件,调用isDirectory(),判断该File对象是否是一个已存在的目录

    4、Path:Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单。如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。

    三、InputStream

    1、InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read():这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了

    2、FileInputStreamInputStream的一个子类。FileInputStream就是从文件流中读取数据。

    3、在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的。应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时地关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行。

      InputStreamOutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源

    4、在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException

    5、如果读取过程中发生了IO错误,InputStream就没法正确地关闭,资源也就没法及时释放。 因此,我们需要用try ... finally来保证InputStream在无论是否发生IO错误的时候都能够正确地关闭。

    public void readFile() throws IOException {
        InputStream input = null;
        try {
            input = new FileInputStream("src/readme.txt");
            int n;
            while ((n = input.read()) != -1) { // 利用while同时读取并判断
                System.out.println(n);
            }
        } finally {
            if (input != null) { input.close(); }
        }
    }

      用try ... finally来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源。推荐的写法如下:

    public void readFile() throws IOException {
        try (InputStream input = new FileInputStream("src/readme.txt")) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println(n);
            }
        } // 编译器在此自动为我们写入finally并调用close()
    }

      实际上,编译器并不会特别地为InputStream加上自动关闭。编译器只看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。InputStreamOutputStream都实现了这个接口,因此,都可以用在try(resource)中。

    6、缓冲区

      在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多InputStream提供了两个重载方法来支持读取多个字节:

    • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
    • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数

      利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。

    7、阻塞:在调用InputStreamread()方法读取数据时,我们说read()方法是阻塞(Blocking)的。它的意思是,对于下面的代码:

    int n;
    n = input.read(); // 必须等待read()方法返回才能执行下一行代码
    int m = n;

      执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。

    四、OutputStream

    1、和InputStream相反,OutputStream是Java标准库提供的最基本的输出流。和InputStream类似,OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分(相当于b & 0xff)。

      和InputStream类似,OutputStream也提供了close()方法关闭输出流,以便释放系统资源。

      要特别注意:OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。

    2、为什么要有flush()?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream有个flush()方法,能强制把缓冲区内容输出。

    3、通常情况下,我们不需要调用这个flush()方法,因为缓冲区写满了OutputStream会自动调用它,并且,在调用close()方法关闭OutputStream之前,也会自动调用flush()方法。但是,在某些情况下,我们必须手动调用flush()方法。举个栗子:

      小明正在开发一款在线聊天软件,当用户输入一句话后,就通过OutputStreamwrite()方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么肥四?原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。

      解决办法就是每输入一句话后,立刻调用flush(),不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去。

    4、Java标准库的java.io.OutputStream定义了所有输出流的超类:

    • FileOutputStream实现了文件流输出;

    • ByteArrayOutputStream在内存中模拟一个字节流输出。

      某些情况下需要手动调用OutputStreamflush()方法来强制输出缓冲区。

      总是使用try(resource)来保证OutputStream正确关闭。

    五、Filter模式

    1、通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合

                     ┌─────────────┐
                     │ InputStream │
                     └─────────────┘
                           ▲ ▲
    ┌────────────────────┐ │ │ ┌─────────────────┐
    │  FileInputStream   │─┤ └─│FilterInputStream│
    └────────────────────┘ │   └─────────────────┘
    ┌────────────────────┐ │     ▲ ┌───────────────────┐
    │ByteArrayInputStream│─┤     ├─│BufferedInputStream│
    └────────────────────┘ │     │ └───────────────────┘
    ┌────────────────────┐ │     │ ┌───────────────────┐
    │ ServletInputStream │─┘     ├─│  DataInputStream  │
    └────────────────────┘       │ └───────────────────┘
                                 │ ┌───────────────────┐
                                 └─│CheckedInputStream │
                                   └───────────────────┘

    2、Java的IO标准库使用Filter模式为InputStreamOutputStream增加功能:

    • 可以把一个InputStream和任意个FilterInputStream组合;

    • 可以把一个OutputStream和任意个FilterOutputStream组合。

      Filter模式可以在运行期动态增加功能(又称Decorator模式)。

    六、操作ZIP

     1、ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容。

    ┌───────────────────┐
    │    InputStream    │
    └───────────────────┘
              ▲
              │
    ┌───────────────────┐
    │ FilterInputStream │
    └───────────────────┘
              ▲
              │
    ┌───────────────────┐
    │InflaterInputStream│
    └───────────────────┘
              ▲
              │
    ┌───────────────────┐
    │  ZipInputStream   │
    └───────────────────┘
              ▲
              │
    ┌───────────────────┐
    │  JarInputStream   │
    └───────────────────┘

    2、另一个JarInputStream是从ZipInputStream派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF文件。因为本质上jar包就是zip包,只是额外附加了一些固定的描述文件。

    3、ZipInputStream可以读取zip格式的流,ZipOutputStream可以把多份数据写入zip包;配合FileInputStreamFileOutputStream就可以读写zip文件。

    七、读取classpath资源

    1、很多Java程序启动的时候,都需要读取配置文件。但是,从磁盘的固定目录读取配置文件,不是一个好的办法。有没有路径无关的读取文件的方式呢?

    2、Java存放.class的目录或jar包也可以包含任意其他类型的文件,例如:

    • 配置文件,例如.properties
    • 图片文件,例如.jpg
    • 文本文件,例如.txt.csv
    • ……

      因此,从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties文件放到classpath中,就不用关心它的实际存放路径。

    3、在classpath中的资源文件,路径总是以开头,我们先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件

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

      调用getResourceAsStream()需要特别注意的一点是,如果资源文件不存在,它将返回null。因此,我们需要检查返回的InputStream是否为null,如果为null,表示资源文件在classpath中没有找到。

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

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

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

    5、小结:

      把资源存储在classpath中可以避免文件路径依赖;

      Class对象的getResourceAsStream()可以从classpath中读取指定资源;

      根据classpath读取资源时,需要检查返回的InputStream是否为null

    八、序列化

    1、序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。

    2、为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。

    3、有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。

    4、有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。

    5、如何把一个Java对象序列化:

    (1)必须实现一个特殊的java.io.Serializable接口:public interface Serializable { }Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

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

    public class Main {
        public static void main(String[] args) throws IOException {
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
                // 写入int:
                output.writeInt(12345);
                // 写入String:
                output.writeUTF("Hello");
                // 写入Object:
                output.writeObject(Double.valueOf(123.456));
            }
            System.out.println(Arrays.toString(buffer.toByteArray()));
        }
    }

    6、反序列化 - 和ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象

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

    7、安全性

      因为Java的序列化机制可以导致一个实例能直接从byte[]数组创建,而不经过构造方法,因此,它存在一定的安全隐患。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。

      实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。

    8、小结:

      可序列化的Java对象必须实现java.io.Serializable接口,类似Serializable这样的空接口被称为“标记接口”(Marker Interface);

      反序列化时不调用构造方法,可设置serialVersionUID作为版本号(非必需);

      Java的序列化机制仅适用于Java,并且存在安全隐患,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON。

    九、Reader

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

    InputStreamReader
    字节流,以byte为单位 字符流,以char为单位
    读取字节(-1,0~255):int read() 读取字符(-1,0~65535):int read()
    读到字节数组:int read(byte[] b) 读到字符数组:int read(char[] c)

    2、FileReaderReader的一个子类,它可以打开文件并获取Reader

    3、如果我们读取一个文本文件,如果文件中包含中文,就会出现乱码,因为FileReader默认的编码与系统相关,例如,Windows系统的默认编码可能是GBK,打开一个UTF-8编码的文本文件就会出现乱码。要避免乱码问题,我们需要在创建FileReader时指定编码:

    Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);

    4、和InputStream类似,Reader也是一种资源,需要保证出错的时候也能正确关闭,所以我们需要用try (resource)来保证Reader在无论有没有IO错误的时候都能够正确地关闭。

    5、也有缓冲区,也阻塞

    6、ReaderInputStream有什么关系?

      除了特殊的CharArrayReaderStringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream

    7、既然Reader本质上是一个基于InputStreambytechar的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader

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

    8、构造InputStreamReader时,我们需要传入InputStream,还需要指定编码,就可以得到一个Reader对象。

    try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
        // TODO:
    }

    十、Writer

    1、Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。

    OutputStreamWriter
    字节流,以byte为单位 字符流,以char为单位
    写入字节(0~255):void write(int b) 写入字符(0~65535):void write(int c)
    写入字节数组:void write(byte[] b) 写入字符数组:void write(char[] c)
    无对应方法 写入String:void write(String s)

    2、Writer定义了所有字符输出流的超类:

    • FileWriter实现了文件字符流输出;

    • CharArrayWriterStringWriter在内存中模拟一个字符流输出。

    3、使用try (resource)保证Writer正确关闭。

    4、Writer是基于OutputStream构造的,可以通过OutputStreamWriterOutputStream转换为Writer,转换时需要指定编码。

    十一、PrintStream和PrintWriter

      PrintStream是一种能接收各种数据类型的输出,打印数据时比较方便:

    • System.out是标准输出;
    • System.err是标准错误输出。

      PrintWriter是基于Writer的输出。

    十二、使用Files

      对于简单的小文件读写操作,可以使用Files工具类简化代码。Java提供了FilesPaths这两个工具类,能极大地方便我们读写文件。

    // 我们要把一个文件的全部内容读取为一个byte[],可以这么写:
    byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));
    
    // 如果是文本文件,可以把一个文件的全部内容读取为String:
    
    // 默认使用UTF-8编码读取:
    String content1 = Files.readString(Paths.get("/path/to/file.txt"));
    // 可指定编码:
    String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.ISO_8859_1);
    // 按行读取并返回每行内容:
    List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));
    // 写入文件也非常方便:
    
    // 写入二进制文件:
    byte[] data = ...
    Files.write(Paths.get("/path/to/file.txt"), data);
    // 写入文本并指定编码:
    Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
    // 按行写入文本:
    List<String> lines = ...
    Files.write(Paths.get("/path/to/file.txt"), lines);

      此外,Files工具类还有copy()delete()exists()move()等快捷方法操作文件和目录。

      需要特别注意的是,Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。

      读写大型文件仍然要使用文件流,每次只读写一部分文件内容。

  • 相关阅读:
    build-your-own-react 注释版
    react hooks 理念之代数效应
    用Robot Framework+python来测试基于socket通讯的C/S系统(网络游戏)
    使用svndumpfilter exclude来清理svn库的废弃文件实现差别备份
    使用SonarQube+Eclipse来分析python代码
    是该写点什么了
    EventLoop
    Promise 学习
    ES6 模块化导入导出
    Vue实现对象列表根据某一字段排序
  • 原文地址:https://www.cnblogs.com/goloving/p/14817332.html
Copyright © 2020-2023  润新知