2.1 IO详解
本文将介绍Java语言中与I/O操作相关的内容,主要是流式I/O,关于NIO,我们之后会介绍。
2.1.1 流式IO
JavaI/O中的一个基本概念是流,包括输入流和输出流。简单的来说,流是一个连续的字节序列,可以从输入流读取这个字节序列,也可以将字节序列写入到输出流。输入流和输出流所操纵的基本单元就是字节,每次读取和写入单个字节或是字节数组。基本字节流所提供的一般是更通用、更底层的读写功能:比如InputStream只提供了顺序读取、跳过部分字节和标记、重置的支持,而OutputStream只能顺序写入。一般我们会将其包装一下使用:
- 如果操作的是Java的基本数据类型,可以使用DataInputStream和DataOutputStream,它们所提供的类似readFloat和writeFloat这样的方法,以简化会基本数据类型的读写
- 如果操作的是Java中的对象类型,可以使用ObjectInputStream和ObjectOutputStream,它们与对象的序列化机制一起实现Java对象状态的持久化和数据传递
资源管理
由于I/O操作都是有限的资源,每个打开的流都需要被正确的关闭以释放资源,一般谁打开谁释放。如果一个方法只是作为流的使用者,就不需要考虑流的关闭问题,典型的情况是在servlet实现中并不需要关闭HttpServletResponse中的输出流。
在使用输入流的过程中,经常会遇到需要复用一个输入流的情况,即多次读取一个输入流中的内容。为了保证每个使用流的对象都能获取到正确的内容,需要对流进行一定的处理。通常有两种解决的办法:
- 一种是利用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方法重置即可。
- 另外一种做法是把输入流的内容转换成字节数组,如下所示:
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。