[Interview]Java 面试宝典系列之 Java IO 流

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Interview]Java 面试宝典系列之 Java IO 流相关的知识,希望对你有一定的参考价值。

文章目录

1. 介绍一下 Java 中的 IO 流

IO(Input Output)用于实现对数据的输入与输出操作,Java 把不同的输入/输出源(键盘、文件、网络等)抽象表述为流(Stream)。流是从起源到接收的有序数据,有了它程序就可以采用同一方式访问不同的输入/输出源。流的分类如下:

  1. 数据流向:输入流与输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据

  2. 数据单位:字节流和字符流,其中字节流操作的数据单元是 8 位的字节,而字符流(Unicode 编码)操作的数据单元是 16 位(2 字节)的字符

  3. 流的角色:字节流和处理流(包装流),其中节点流可以直接从/向一个特定的 IO 设备(磁盘、网络等)读/写数据,也称为低级流,而处理流是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也称为高级流

  4. 处理流设计理念体现了修饰器模式。列表中前 5 行为节点流,其余行对应的流为处理流

    分类字节输入流字节输出流字符输入流字符输出流
    1抽象基类InputStreamOutputStreamReaderWriter
    2访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
    3访问数组ByteArrayInputSteamByteArrayOutputStramCharArrayReaderCharArrayWriter
    4访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
    5访问字符串StringReaderStringWriter
    缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
    转换流InputStreamReaderOutputStreamWriter
    对象流ObjectInputStreamObjectOutputStream
    抽象基类FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
    打印流PirntStreamPrintWriter
    推回输入流PushbackInputStreamPushbackReader
    特殊流DataInputStreamDataOutputStream
    • File 开头的文件流用于访问文件
    • ByteArray/CharArray 开头的流用于访问内存中的数组
    • Piped 开头的管道流用于访问管道,实现进程之间的通信
    • String 开头的流用于访问内存中的字符串
    • Buffered 开头的缓冲流,用于在读写数据时对数据进行缓存,以减少 IO 次数
    • InputStreamReader、InputStreamWriter 是转换流,用于将字节流转换为字符流
    • Object 开头的流是对象流,用于实现对象的序列化
    • Print 开头的流是打印流,用于简化打印操作
    • Pushback 开头的流是推回输入流,用于将已读入的数据推回到缓冲区,从而实现再次读取
    • Data 开头的流是特殊流,用于读写 Java 基本类型的数据

2. 怎么用流打开一个大文件?

打开大文件,应避免直接将文件中的数据全部读取到内存中,可以采用分次读取的方式:

  1. 使用缓冲流。缓冲流内部维护了一个缓冲区,通过与缓冲区的交互,减少与设备的交互次数
  2. 使用 NIO(Non-blocking I/O,在 Java 领域也称为 New I/O)。NIO 采用内存映射文件的方式来处理输入/输出,其将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多

3. 说说 NIO 的实现原理

Java 的 NIO 主要由三个核心部分组成:Channel、Buffer、Selector

  1. Channel :基本上所有的 IO 在 NIO 中都从一个 Channel 开始,数据可以从 Channel 读到 Buffer 中,也可以从 Buffer 写到 Channel 中。Channel 有好几种类型,其中比较常用的有 FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel 等,分别对应 File、UDP、TCP

  2. Buffer:Buffer 本质上是一块可以读、写数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法用来方便地访问该块内存。Java NIO 里关键的 Buffer 实现有 CharBuffer、ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。这些 Buffer 覆盖了你能通过 IO 发送的基本数据类型,即 byte、short、int、long、float、double、char。Buffer 对象包含三个重要的属性,分别是 capacity、position、limit,其中 position 和 limit 的含义取决于 Buffer 是处在读模式还是写模式写,而 capacity 的含义总是一样的

    • capacity:作为一个内存块,Buffer 有个固定的最大值就是 capacity。Buffer 只能写 capacity 个数据,一旦 Buffer 满了需要将其清空才能继续往里写数据

    • position:当写数据到 Buffer 中时,position 表示当前的位置。初始的 position 值为 0。当一个数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单元,position 最大可为 capacity–1。当将 Buffer 从写模式切换到读模式,position 会被重置为 0,从 Buffer 的 position 处读取数据时,position 向前移动到下一个可读的位置

    • limit:在写模式下,Buffer 的 limit 表示最多能往 Buffer 里写多少数据,此时 limit 等于 capacity。当切换 Buffer 到读模式时, limit 表示你最多能读到多少数据,此时 limit 会被设置成写模式下的 position 值

  3. Selector:Selector 允许单线程处理多个 Channel,如果你的应用打开了多个连接(通道),但每个连接的流量都很低,这时使用 Selector 就会很方便。要使用 Selector 就必须向 Selector 注册 Channel,然后调用它的 select() 方法,该方法会一直阻塞直到某个注册的通道有事件就绪,一旦这个方法返回,线程就可以处理这些事件(例如有新连接进来,数据接收等)。以下是在一个单线程中使用一个 Selector 处理 3 个 Channel 的示意图:

    public void nio() throws IOException 
        RandomAccessFile randomAccessFile = new RandomAccessFile("C:/Users/Admin/Desktop/nio.txt", "rw");
        // 文件通道
        FileChannel fileChannel = randomAccessFile.getChannel();
        // 分配字节缓冲区容量,设置缓冲区 capacity 的值
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 从通道中将数据读入到字节缓冲区中,返回读取到的字节数
        int bytesRead = fileChannel.read(byteBuffer);

        while (bytesRead != -1) 
            //  设置缓冲区的 limit = position; position = 0;
            byteBuffer.flip();
            // 循环读取缓冲区中的所有内容
            while (byteBuffer.hasRemaining()) 
                System.out.print((char) byteBuffer.get());
            

            /* ------------------------------ buffer.clear() 和 buffer.compact() 的区别 ------------------------------ */
            //                              buffer.clear(); => position = 0; limit = capacity;
            // Buffer 被清空了,但 Buffer 中的数据并未被清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据。
            // 如果 Buffer 中有一些未读的数据,此时调用 clear() 方法,这些数据将 “被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。
            //==========================================================================================================
            //                              buffer.compact(); => limit = capacity;
            // 如果 Buffer 中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么可以使用 compact() 方法。
            // compact() 方法将所有未读的数据拷贝到 Buffer 起始处,然后将 position 设到最后一个未读元素的后面,现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。
            byteBuffer.compact();
            /* ------------------------------- buffer.mark() 和 buffer.rewind() 的区别 ------------------------------- */
            // buffer.mark():可以标记 Buffer 中的一个特定的 position,之后可以通过调用 Buffer.reset() 方法恢复到这个 position
            // buffer.rewind():将 position 设回 0,所以你可以重读 Buffer 中的所有数据。limit 保持不变,仍然表示能从 Buffer 中读取多少个元素

            // 继续从通道中读取数据到缓冲区
            bytesRead = fileChannel.read(byteBuffer);
        
    

扩展阅读之不同操作系统下 Java NIO 的 Selector 的具体实现:

Java NIO 根据操作系统不同, 针对 NIO 中的 Selector 有不同的实现,Oracle JDK 会自动选择合适的 Selector,也可以自行设置特定的 Selector:-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider

  • macosx:KQueueSelectorProvider
  • solaris:DevPollSelectorProvider
  • Linux:EPollSelectorProvider (Linux kernels >= 2.6) 或 PollSelectorProvider
  • windows:WindowsSelectorProvider

JDK 在 Linux 中默认使用 epoll 方式,但 JDK 的 epoll 采用的是水平触发,所以自 Netty 4.0.16 起,Netty 重新实现了 epoll 机制,为 Linux 通过 JNI 的方式提供了 native socket transport,具有以下优点:

  1. 采用边缘触发方式
  2. netty epoll transport 暴露了更多的 nio 没有的配置参数,如 TCP_CORK,SO_REUSEADDR 等等
  3. 优化代码,更少 GC,更少 synchronized

4. 介绍一下 Java 的序列化与反序列化

  • 序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。对象的序列化(Serialize)是指将一个 Java 对象写入 IO 流中,对象的反序列化(Deserialize)则是指从 IO 流中恢复该 Java 对象

  • 若对象要支持序列化机制,则它的类需要实现 Serializable 接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的,Java 的很多类已经实现了 Serializable 接口,如八大包装类、String、Date 等

  • 若对象要实现序列化,则需要使用对象流 ObjectInputStreamObjectOutputStream。其中,在序列化时需要调用ObjectOutputStream 对象的 writeObject() 方法,以输出对象序列。在反序列化时需要调用 ObjectInputStream 对象的 readObject() 方法,将对象序列恢复为对象

5. Serializable 接口为什么需要定义 serialVersionUID 变量?

serialVersionUID 代表序列化的版本,通过定义类的序列化版本,在反序列化时只要对象中所存的版本和当前类的版本一致,就允许做数据恢复的操作,否则将会抛出序列化版本不一致的异常 java.io.InvalidClassException

如果不定义序列化版本,在反序列化容易抛出序列化版本不一致的异常,比如:

  1. 创建该类的实例,并将这个实例序列化,保存在磁盘上
  2. 升级这个类,例如增加、删除、修改这个类的成员变量
  3. 反序列化该类的实例,即从磁盘上恢复修改之前保存的数据

在第 3 步恢复数据的时候,当前的类已经和序列化的数据的格式产生了冲突,可能会发生各种意想不到的问题。增加了序列化版本之后,在这种情况下依然可以尝试反序列化该对象,提高了数据的安全性

6. 除了 Java 自带的序列化之外,你还了解哪些序列化工具?

  1. JSON:目前使用比较频繁的格式化数据工具,简单直观,可读性好,有 jackson,gson,fastjson
  2. Protobuf:一个用来序列化结构化数据的技术,支持多种语言诸如 C++、Java 以及 Python,可以使用该技术来持久化数据或者序列化成网络传输的数据
    • 优点:相比较一些其他的 XML 技术而言,该技术的一个明显特点就是更加节省空间(以二进制流存储)、速度更快以及更加灵活
    • 缺点:Protobuf 支持的数据类型相对较少,不支持常量类型。由于其设计的理念是纯粹的展现层协议(Presentation Layer),目前并没有一个专门支持 Protobuf 的 RPC 框架
  3. Thrift:是 Facebook 开源的一个高性能,轻量级 RPC 服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。Thrift 并不仅仅是序列化协议,还是一个 RPC 框架
    • 优点:相对于 JSON 和 XML 而言,Thrift 在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的 RPC 解决方案
    • 缺点:由于 Thrift 的序列化被嵌入到 Thrift 框架里, Thrift 框架本身并没有透明提供序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如 HTTP)
  4. Avro:提供两种序列化格式,即 JSON 格式或 Binary 格式。
    • Binary 格式在空间开销和解析性能方面可以和 Protobuf 相媲美, JSON 格式方便测试阶段的调试
    • Avro 支持的数据类型非常丰富,甚至包括了 C++ 语言里面的 union 类型
    • Avro 支持 JSON 格式的 IDL 和类似于 Thrift 和 Protobuf 的 IDL(实验阶段),这两者之间可以互转
    • Schema 可以在传输数据的同时发送,加上 JSON 的自我描述属性,这使得 Avro 非常适合动态类型语言
    • Avro 在做文件持久化的时候,一般会和 Schema 一起存储,所以 Avro 序列化文件自身具有自我描述属性,所以非常适合于做 Hive、Pig 和 MapReduce 的持久化数据格式。对于不同版本的 Schema,在进行 RPC 调用的时候,服务端和客户端可以在握手阶段对 Schema 进行互相确认,大大提高了最终的数据解析速度

7. 如果不用 JSON 工具,该如何实现对实体类的序列化?

  1. 可以使用 Java 原生的序列化机制,但是效率比较低,适合小项目

  2. 可以使用其他的一些第三方类库,比如 Protobuf、Thrift、Avro 等

以上是关于[Interview]Java 面试宝典系列之 Java IO 流的主要内容,如果未能解决你的问题,请参考以下文章

[Interview]Java 面试宝典系列之 Spring Boot

[Interview]Java 面试宝典系列之 Java 多线程

[Interview]Java 面试宝典系列之 MyBatis

[Interview]Java 面试宝典系列之 Java 集合类

[Interview]Java 面试宝典系列之 JavaWeb

[Interview]Java 面试宝典系列之 Spring MVC