IO详解

作者: jianhuih | 来源:发表于2017-02-14 14:51 被阅读0次

2.1 IO详解

本文将介绍Java语言中与I/O操作相关的内容,主要是流式I/O,关于NIO,我们之后会介绍。

2.1.1 流式IO

JavaI/O中的一个基本概念是流,包括输入流和输出流。简单的来说,流是一个连续的字节序列,可以从输入流读取这个字节序列,也可以将字节序列写入到输出流。输入流和输出流所操纵的基本单元就是字节,每次读取和写入单个字节或是字节数组。基本字节流所提供的一般是更通用、更底层的读写功能:比如InputStream只提供了顺序读取、跳过部分字节和标记、重置的支持,而OutputStream只能顺序写入。一般我们会将其包装一下使用:

  1. 如果操作的是Java的基本数据类型,可以使用DataInputStream和DataOutputStream,它们所提供的类似readFloat和writeFloat这样的方法,以简化会基本数据类型的读写
  2. 如果操作的是Java中的对象类型,可以使用ObjectInputStream和ObjectOutputStream,它们与对象的序列化机制一起实现Java对象状态的持久化和数据传递

资源管理

由于I/O操作都是有限的资源,每个打开的流都需要被正确的关闭以释放资源,一般谁打开谁释放。如果一个方法只是作为流的使用者,就不需要考虑流的关闭问题,典型的情况是在servlet实现中并不需要关闭HttpServletResponse中的输出流。

在使用输入流的过程中,经常会遇到需要复用一个输入流的情况,即多次读取一个输入流中的内容。为了保证每个使用流的对象都能获取到正确的内容,需要对流进行一定的处理。通常有两种解决的办法:

  1. 一种是利用InputStream的标记支持,如果一个流支持标记的话(通过markSupported方法判断),就可以在流开始的地方通过mark方法添加一个标记,当完成一次对流的使用之后,通过reset方法就可以把流的读取位置重置到上次标记的位置,如此就可以复用这个输入流。大部分输入流是不支持标记的,可以通过BufferedInputStream进行包装来支持标记。在NanoHttpD源码里关于Http头部解析使用的就是该方法。
private InputStream prepareStream(InputStream is) {
    BufferedInputStream buffered = new BufferedInputStream(is);
    buffered.mark(Integer.MAX_VALUE);
    return buffered;
}
private void resetStream(InputStream is) throws IOException {
    is.reset();
    is.mark(Integer.MAX_VALUE);
}

如上面的代码所示,通过prepareStream方法可以用一个BufferedInputStream来包装基本的InputStream,通过mark方法在流开始的时候添加一个标记,允许读入Integer.MAX_VALUE个字节。每次流使用完成之后,通过reset方法重置即可。

  1. 另外一种做法是把输入流的内容转换成字节数组,如下所示:
private byte[] saveStream(InputStream input) throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    ReadableByteChannel readChannel = Channels.newChannel(input);
    ByteArrayOutputStream output = new ByteArrayOutputStream(32 * 1024);
    WritableByteChannel writeChannel = Channels.newChannel(output);
    while ((readChannel.read(buffer)) > 0 || buffer.position() != 0) {
        buffer.flip();
        writeChannel.write(buffer);
        buffer.compact();
    }
    return output.toByteArray();
}

上面的代码中saveStream方法把一个InputStream保存为字节数组。

这两种做法的思路其实是相似的,BufferedInputStream在内部也创建了一个字节数组来保存从原始输入流中读入的内容。使用字节数组不需要考虑资源相关的问题,可以尽早的关闭原始的输入流,而无需等待所有使用流的操作完成。

缓冲区

由于流背后的数据有可能比较大,在实际的操作中,通常会使用缓冲区来提高性能。传统的缓冲区一般是使用数组来实现的的。比如经典的从InputStream到OutputStream的复制的实现,就是使用一个字节数组作为中间的缓冲区。

总结

流式IO:两个对应一个桥梁。字节流和字符流的对应(IOStream和Reader/Writer);输入流和输出流的对应。一个桥梁:从字节流到字符流的桥梁(InputStreamReader和OutputStreamWriter)。流的具体类又可以分为:介质流和过滤流。介质流是一些基本流,主要是从具体的介质上读取数据:例如文件,内存缓冲区(Byte数组、Char数组)等。过滤流:主要是装饰器类装饰具体的介质流。需要注意的是:流是一维单向(RandomAccessFile就是解决单向的问题)

这里通过一个表格简单介绍一下常用的IO流,这里以输入字节流为例,输出字节流与其相对应,这里就不再详述了

  • 基本输入字节流:
功能 具体使用
ByteArrayInputStream 将内存中的Byte数组适配为一个InputStream 作为数据源,可以和其他装饰流配合,建议加入缓冲功能
FileInputStream 最基本的文件输入流。主要用于从文件中读取信息 作为数据源,可以和其他装饰流配合
PipedInputStream 读取从对应PipedOutputStream写入的数据,在流中实现了管道的概念 在多线程程序中作为数据源,可以和其他装饰流配合
  • 装饰输入字节流:
功能 具体使用
BufferedInputStream 正常情况下,每读取一个字节都要进行一次IO,使用该对象后可以进行一次IO将字节流读取缓存区中,从缓存区读取 使用InputStream的方法读取,只是背后多一个缓存的功能,设计模式中透明装饰器的应用。
DataInputStream 一般和DataOutputStream配对使用,完成基本数据类型的读写 提供了大量的读取基本数据类新的读取方法
ObjectInputStream 一般和ObjectOutputStream配对使用,完成Java对象类型的读写 对象的序列化机制一起,可以实现Java对象状态的持久化和数据传递

2.1.2 字符与编码

字节和字符:字节是计算机数据表示(8位二进制数据,可以认为是二进制码),而字符就是用户直接看见的信息。
字符集和编码:字符集就是字符的集合,一个字符集中所包含的字符通常与地区和语言有关,常见的字符集有ASCII和Unicode等(这里需要两者的区别,ASCII不仅是字符集,还指定了编码方案,而Unicode仅仅是一个字符集)。对于字符集中的每个字符,为了在计算机中存储和网络的传递,都需要转换某种字节的序列(比如ASCII编码中字符A的编码就是0x41),即该字符的编码,同一个字符集可以有不同的编码方式(比如Unicode的编码方案有UTF-8和UTF-16等)。如果某种编码格式产生的字节序列,用另外一种编码格式来解码的话,就可能会得到错误的字符,从而产生乱码的情况。所以将一个字节序列转换成字符串的时候,需要知道正确的编码格式。
NIO中的java.nio.charset包提供了与字符集相关的类,可以用来进行编码和解码。其中的CharsetEncoder和CharsetDecoder允许对编码和解码过程进行精细的控制,如处理非法的输入以及字符集中无法识别的字符等。通过这两个类可以实现字符内容的过滤,比如应用程序在设计的时候就只支持某种字符集,如果用户输入了其它字符集中的内容,在界面显示的时候就是乱码,对于这种情况,可以在解码的时候忽略掉无法识别的内容。

String input = "你123好";
Charset charset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();
encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
CharsetDecoder decoder = charset.newDecoder();
CharBuffer buffer = CharBuffer.allocate(32);
buffer.put(input);
buffer.flip();
try {
    ByteBuffer byteBuffer = encoder.encode(buffer);
    CharBuffer cbuf = decoder.decode(byteBuffer);
    System.out.println(cbuf); //输出123
} catch (CharacterCodingException e) {
    e.printStackTrace();
}

上面的代码中,通过使用ISO-8859-1字符集的编码和解码器,就可以过滤掉字符串中不在此字符集中的字符。
JavaI/O在处理字节流之外,还提供了处理字符流的类,即Reader/Writer类及其子类,它们所操纵的基本单位是char类型。在字节和字符之间的桥梁就是编码格式,通过编码器来完成这两者之间的转换。在创建Reader/Writer子类实例的时候,应该总是使用两个参数的构造方法,即显式指定使用的字符集或编码解码器。如果不显式指定,使用的是JVM的默认字符集,有可能在其它平台上产生错误。

2.1.3 辅助工具类

File,RandomAccessFile以及FileDescriptor类

由于JavaIO使用过于繁琐,更多使用的是一些第三方的类库进行操作:例如流式IO方面,一般多用来操作本地文件,使用Apache的CommonsIO。

相关文章

  • Java之IO流详解

    title: Java之IO流详解tags: Java IO流categories: Java IO流 大多数应用...

  • IO多路复用

    参考:1 IO多路复用机制详解

  • iOS资源汇总

    iOS·Objective-C UI控件详解整理 iOS UI控件详解—「UIScrollView滚动视图」 iO...

  • IO详解

    2.1 IO详解 本文将介绍Java语言中与I/O操作相关的内容,主要是流式I/O,关于NIO,我们之后会介绍。 ...

  • 产品开发

    IO复用模型同步,异步,阻塞,非阻塞及实例详解 IO复用模型同步,异步,阻塞,非阻塞及实例详解数据库中间件 MyC...

  • ios storyboard实现抽屉式侧栏

    MMDrawerController搭配storyboard使用详解MMDrawerController是一个iO...

  • 2018-04-25

    java.io(Input/Output)使用详解 目录1、流(Stream)2、流的分类3、java.io包下的...

  • Linux IO实时监控iostat命令详解

    简介Linux IO实时监控iostat命令详解iostat -x 1 查看磁盘的IO负载 iostat主要用于监...

  • IO多路复用

    参考资料:[1]. 聊聊Linux 五种IO模型[2]. 聊聊IO多路复用之select、poll、epoll详解...

  • 几个反直觉的概率问题

    ----------- 上篇文章 洗牌算法详解[https://labuladong.gitbook.io/alg...

网友评论

      本文标题:IO详解

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