Java BIO和NIO
[TOC]
BIO
自Java被发明以来,IO都是其中很重要的一块。最初伴随1.0的IO,到1.4出现的NIO再到1.7出现的NIO2,充分说明这个部分的重要性。
本文仅总结1.0~1.3版的基于字节流的流IO。
概述
-
简而言之,IO的目的就是对数据源进行读写。数据源可以有很多种,常见的数据源有磁盘(即最常见的文件),网络(这类数据的访问需要借助网络),内存(运行时数据)。读写的操作,是相对于计算机而言的,读是从文件读入程序,写是从程序写出到“文件”(这里的文件是抽象意义的文件)。
-
计算机只认识0和1,所以数据在计算机中的存储形式,是二进制的,最后在显示器呈现出来各种形式,都是对二进制数据进行加工再展示的。IO操作从数据源读数据,都是基于二进制(或者说是字节),读出来的都是01的二进制序列,然后在内存中选择字符集进行编码,才得到易读的字符串。写数据也是一样,不管在内存中是视频音乐还是文本字符串,写入的时候都要转成二进制序列(字节序列)写进“文件”中。
-
1.4版之前的IO,核心部分有三块
- 数据集合的抽象:java.io.File类
- 包裹输入行为抽象的InputStream类
- 包裹输出行为的抽象的OutPutStream类
File类,它是一种抽象,一个File实例指向一个二进制序列的集合。
InputStream和OutPutStream是直接和字节序列交互的通道,可以在这些通道上施加行为,使得字节流可以在数据源和程序之间流动,达到操作数据的目的。除了这两个流之外,其他的XXStream,XXReader/Writer实际上都是基于这两个抽象字节流的,字符流,是对字节流加工(装饰)后的可读形式,依据数据流动的方向进行了相对应的编码(字节->字符)和解码(字符->字节)操作。
java.io.File类
File类是对文件路径的封装,并提供了一些文件操作的方法(并没有读写的内容操作)。
看一下File类的构造器:
public File(String pathname) {
if (pathname == null) {
throw new NullPointerException();
}
this.path = fs.normalize(pathname);
this.prefixLength = fs.prefixLength(this.path);
}
这个构造器是最常用的,一般创建一个文件对象都是使用文件的路径来创建。从此处可以看出来,File类包装的是文件的路径。
还有其他的构造器:
public File(String parent, String child)
public File(File parent, String child)
public File(URI uri)
File类封装了路径,并提供了一些基于路径的方法,比如获取上层文件,获取系统分隔符,获取绝对路径,文件是否存在,判断是文件还是目录,设置和查看文件属性,查看目录的使用空间,列出目录下的所有文件,创建文件和目录,删除文件,重命名文件等等。
需要注意的是,创建一个File对象,只是代表一个路径,并不会对应的创建文件,创建文件需要显式地调用创建文件的方法(file.createNewFile()
)或者将其绑定到输出流。
读
1.InputStream
要读数据,需要借助输入流,使数据流入程序中。
InputStream是一个抽象类,它核心的抽象方法是public abstract int read() throws IOException;
此方法返回读取的一个字节,如果读到数据末尾,就返回-1。
下面的两个read是将数据读入字节数组:
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
// 每次读入一个字节,将读入的字节存储到byte数组中。如果有数据,返回读到的字节数,否则返回-1;
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
2.InputStream的装饰流
Java IO模块可以说是装饰器模式的经典案例了。
输入流有众多的对于不同需求的类:
字节流(读)
字节流简要说明
- java.io.FileInputStream 1.0
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
public FileInputStream(File file) throws FileNotFoundException {
}
public FileInputStream(FileDescriptor fdObj) {
}
FileInputStream不是InputStream的装饰类,只是其一个子类,其内部并没有聚合一个InputStream实例,且它的read()方法是native方法,主要用于文件流的字节读取。当然,它作为一个InputStream,是可以被其他类装饰的。
- java.io.ByteArrayInputStream 1.0
public ByteArrayInputStream(byte buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
public ByteArrayInputStream(byte buf[], int offset, int length) {
this.buf = buf;
this.pos = offset;
this.count = Math.min(offset + length, buf.length);
this.mark = offset;
}
这个流不读取实际文件中的内容,而是读取内存中的字节数组。
- javax.sound.sampled.AudioInputStream 1.3
public AudioInputStream(InputStream stream, AudioFormat format, long length)
public AudioInputStream(TargetDataLine line)
读取音频流,实际上只是文件格式不同罢了,读入内存是同等对待的。
- java.io.FilterInputStream 1.0
protected FilterInputStream(InputStream in) //注意是Protected方法
它的功能只是装饰一下InputStream实例,实用的功能都是交给其子类完成的。
- java.io.SequenceInputStream 1.0
public SequenceInputStream(Enumeration<? extends InputStream> e)
public SequenceInputStream(InputStream s1, InputStream s2)
它的作用是按顺序读入流,连接成一个流返回。
- StringBufferInputStream
public StringBufferInputStream(String s)
被废弃了,推荐使用ByteArrayInputStream。
- PipedInputStream
用于线程之间的通信,一个Pipe的InputStream应该和一个PipedOutputStream连接起来,达到线程间通信的目的(一个线程读一个线程写)。
public PipedInputStream(PipedOutputStream src) throws IOException {
this(src, DEFAULT_PIPE_SIZE);
}
public PipedInputStream(PipedOutputStream src, int pipeSize)
throws IOException {
initPipe(pipeSize);
connect(src);
}
public PipedInputStream() {
initPipe(DEFAULT_PIPE_SIZE);
}
public PipedInputStream(int pipeSize) {
initPipe(pipeSize);
}
public void connect(PipedOutputStream src) throws IOException {
src.connect(this);
}
- BufferedInputStream
是带缓冲区的输入流,实现逻辑是将字节先读入字节数组中,然后再从缓冲区读数据,比直接读字节流要高效很多,是很常用的输入流类。
看一下主要属性可以知道实现以及初始缓冲区大小:
private static int DEFAULT_BUFFER_SIZE = 8192;
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
protected volatile byte buf[];
构造器:
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
read:
// 同步方法
public synchronized int read() throws IOException {
if (pos >= count) {
fill();//填充buff数组
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff; // 0xff,取返回字节数据的低八位(也只有八位)
}
- java.util.zip.CheckedInputStream
/*An input stream that also maintains a checksum of the data being read.
* The checksum can then be used to verify the integrity of the input data.
*/
public CheckedInputStream(InputStream in, Checksum cksum) {
super(in);
this.cksum = cksum;
}
使用checksum检查输入流。
-
InflaterInputStream
这个类实现了一个流过滤器,用于以“deflate”压缩格式解压缩数据。 它也被用作其他解压过滤器的基础,比如GZIPInputStream。(既然要读,肯定读解压的数据)
它是GZipInputStream和ZipInputStream类的基础。内部借助java.util.zip.Inflater类实例对压缩文件进行读取。 -
LineNumberInputStream
可以track当前行号的输入过滤流。
已经废弃。
看下它怎么track行号的。
@Deprecated
public
class LineNumberInputStream extends FilterInputStream {
int pushBack = -1;
int lineNumber;
int markLineNumber;
int markPushBack = -1;
//...
public int read() throws IOException {
int c = pushBack;
if (c != -1) {
pushBack = -1;
} else {
c = in.read();
}
// 判断是否是换行符,是就累加行号
// Windows : \r\n
// Linux : \n
// Mac : \r
switch (c) {
case '\r':
pushBack = in.read();
if (pushBack == '\n') {
pushBack = -1;
}
case '\n':
lineNumber++;
return '\n';
}
return c;
}
// ...
}
- PushbackInputStream
此类可以支持在读到不需要的数据时,将字节退回流中。
// 数据是读入buf数组的
public int read() throws IOException {
ensureOpen();
if (pos < buf.length) {
return buf[pos++] & 0xff;
}
return super.read();
}
// 退回,不读
// 实际上是操作的buf数组
public void unread(int b) throws IOException {
ensureOpen();
if (pos == 0) {
throw new IOException("Push back buffer is full");
}
buf[--pos] = (byte)b;
}
public void unread(byte[] b, int off, int len) throws IOException {
ensureOpen();
if (len > pos) {
throw new IOException("Push back buffer is full");
}
pos -= len;
System.arraycopy(b, off, buf, pos, len);
}
注意退回缓冲区的字节,可以在下次重读出来。
- DataInputStream
它不仅支持读取字节(这是最基本的功能),还提供了读取各种基本类型数据的装饰功能:
public final int read(byte b[]) throws IOException
public final int read(byte b[], int off, int len) throws IOException
public final void readFully(byte b[]) throws IOException
public final void readFully(byte b[], int off, int len) throws IOException
public final boolean readBoolean() throws IOException
public final byte readByte() throws IOException
public final int readUnsignedByte() throws IOException
public final short readShort() throws IOException
public final int readUnsignedShort() throws IOException
public final char readChar() throws IOException
public final int readInt() throws IOException
public final long readLong() throws IOException
public final float readFloat() throws IOException
public final double readDouble() throws IOException
public final String readLine() throws IOException
public final String readUTF() throws IOException
public final static String readUTF(DataInput in) throws IOException
适应可读性需求。
- ObjectInputStream
这个类使用于Java对象序列化的,单纯从IO角度看,只是在读取的字节的基础上进行特定规则的解析,组装成内存中的一个Java对象。实际上前面的读字节到字节数组中,也是封装字节流到一个Java对象,只是Java反序列化规则更加复杂罢了。
字符流(读)
字符流字符流实际上是对字节流的封装,且封装成字符串返回给程序。
- Reader
public abstract class Reader implements Readable, Closeable {
protected Object lock;
protected Reader() {
this.lock = this;
}
protected Reader(Object lock) {
if (lock == null) {
throw new NullPointerException();
}
this.lock = lock;
}
public int read(java.nio.CharBuffer target) throws IOException {
int len = target.remaining();
char[] cbuf = new char[len];
int n = read(cbuf, 0, len);
if (n > 0)
target.put(cbuf, 0, n);
return n;
}
public int read() throws IOException {
char cb[] = new char[1];
if (read(cb, 0, 1) == -1)
return -1;
else
return cb[0];
}
public int read(char cbuf[]) throws IOException {
return read(cbuf, 0, cbuf.length);
}
abstract public int read(char cbuf[], int off, int len) throws IOException;
/** Maximum skip-buffer size */
private static final int maxSkipBufferSize = 8192;
/** Skip buffer, null until allocated */
private char skipBuffer[] = null;
public long skip(long n) throws IOException {
// ...
}
// ...
}
从其抽象类可以看出来,Reader内部的缓冲区是Char数组,而不再是byte数组了,且它的read()方法的实现是交给子类去做的。
- InputStreamReader
Reader的子类都是基于此类的,InputStreamReader把一个InputStream转换成一个Reader,使得Reader能和数据源接触,同时read方法的子类实现,靠的就是InputStream的read底层实现。
它是字节流和字符流之间的桥梁,桥梁的基础就是字符编码。
一般使用Reader都是
public void test(){
// 如果不指定编码就使用系统编码
BufferedReader bis = new BufferedReader(new InputStreamReader(new InputStream(),charset));
}
@Test
public void testReader() throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("D://text.txt"), "Utf-8"));
System.out.println(br.readLine());
br.close();
}
InputStreamReader:
private final StreamDecoder sd;
/**
* Creates an InputStreamReader that uses the default charset.
*
* @param in An InputStream
*/
public InputStreamReader(InputStream in) {
super(in);
try {
// 这里把字节流编码,sd读出来的数据就是String
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
// The default encoding should always be available
throw new Error(e);
}
}
public int read() throws IOException {
return sd.read();
}
其他的Reader在功能上和命名类似的InputStream功能相同,不同点在于它读到内存中的被编码成字符串了而已。一个是char一个是byte。
写
1.OutPutStream
和读数据相同,写数据到目标中,也需要一个流,使得数据从内存流入目的地。
OutPutStream是输出流的基础类,它提供了打开字节流并写入字节的功能,其他的输出流以及Writer都是基于此功能包装的,和InputStream家族的设计理念是一致的。
public abstract class OutputStream implements Closeable, Flushable {
public abstract void write(int b) throws IOException;
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
public void flush() throws IOException {
}
public void close() throws IOException {
}
}
核心方法是write(int b)和flush();
write方法写入一个字节到文件中,flush是把缓冲区的数据写入流中(支持buffer的写的情况下),在流关闭时会自动刷入文件中。
底层的write是直接操作文件,一个字节一个字节的写入的当然效率不及先把数据写入缓冲区,然后一次性写入多个字节。
输出流的体系结构如下:
字节流(写)
字节流字符流(写)
字符流输出和输入是相互对应的,注意输出到文件的操作如果使用了buffer,需要flush(在流关闭时会自动flush),否则不会写入文件中。其他不再赘述。
RandomAccessFile类
这个类实现了DataInput和DataOutput接口,所以它具有对文件读和写的功能。在使用时,要设置访问的模式,写还是读。
public RandomAccessFile(String name, String mode)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, mode);
}
它能够随机访问文件位置,依靠的是seek方法:
public void seek(long pos) throws IOException {
if (pos < 0) {
throw new IOException("Negative seek offset");
} else {
seek0(pos);
}
}
private native void seek0(long pos) throws IOException;
示例
package test.file;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharArrayReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.io.SequenceInputStream;
import org.junit.Assert;
import org.junit.Test;
public class IO1Test {
/**
* read
*/
/**
* content: JimmyJimmy\n 123456789\n 10.1\n 20.1\n 1\n 0\n
*/
static File file = new File("D://text.txt");
/**
* FileInputStream
*
* @throws IOException
*/
@Test
public void testFileInPutStream() throws IOException {
InputStream in = null;
try {
in = new FileInputStream(file);
byte[] b = new byte[(int) file.length()];
in.read(b);
System.out.println(new String(b, "utf-8"));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null)
in.close();
}
}
/**
* ByteArrayInputStream
*
* 附加测试了read方法,注意在不知道数据大小的情况下要循环读取直到读到-1为止才是结束。
*
* @throws IOException
*/
@Test
public void testByteArrayIn() throws IOException {
byte[] buf = { 10, 12, 32, 34, 126, 127, -2, -128 };
ByteArrayInputStream bai = new ByteArrayInputStream(buf);
byte b[] = new byte[buf.length];
bai.read(b);
// 读完内容,再读返回-1
Assert.assertTrue(bai.read() == -1);
bai.close();
System.out.println(new String(b));
// 长度小于数据流字节数
byte b2[] = new byte[3];
bai = new ByteArrayInputStream(buf);
System.out.println(bai.read(b2));// 3
System.out.println(bai.read(b2));// 3
System.out.println(bai.read(b2)); // 2
System.out.println(bai.read(b2)); // -1
bai.close();
}
/**
* SequenceInputStream 注意要循环读取才能读到其他的要合并的流,注意read方法返回值的含义
*
* @throws IOException
*/
@Test
public void testSequenceIn() throws IOException {
SequenceInputStream si = new SequenceInputStream(new ByteArrayInputStream(new byte[] { 68, 69, 70 }),
new ByteArrayInputStream(new byte[] { 65, 66, 67 }));
byte[] b = new byte[1];
StringBuilder sb = new StringBuilder();
while (si.read(b) != -1) {// 这样一次读一个字节,相当于si.read()
String s = new String(b, "utf-8");
sb.append(s);
System.out.println(s);
}
si.close();
System.out.println(sb);
Assert.assertTrue("DEFABC".equals(sb.toString()));
// **注意直接equals(sb)是不相等的,可以查看String类的equals方法**
Assert.assertFalse("DEFABC".equals(sb));
}
/**
* PipedOutputStream and PipedInputStream
*
* @throws IOException
*/
@Test
public void testPipedStream() throws IOException {
final PipedOutputStream po = new PipedOutputStream();
// final PipedInputStream pi = new PipedInputStream(po); //或者使用以下方式自己绑定
final PipedInputStream pi = new PipedInputStream();
pi.connect(po);
Thread writer = new Thread(() -> {
System.out.println("writer thread run");
try {
po.write("helloworld".getBytes());
po.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("writer thread finish");
});
Thread reader = new Thread(() -> {
System.out.println("reader thread run");
byte[] b = new byte[128];
try {
while (pi.read(b) != -1) {
System.out.println(new String(b));
}
pi.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("reader thread finish");
});
writer.start();
reader.start();
}
/**
* 测试BufferedInputStream,读大文件时性能比原始FileInputStream要高
*
* @throws IOException
*/
@Test
public void testBufferedIn() throws IOException {
// DEFAULT_BUFFER_SIZE = 8192;
BufferedInputStream bi = new BufferedInputStream(new FileInputStream(file), 1024);
byte[] b = new byte[(int) file.length()];
while ((bi.read(b)) != -1) {
System.out.println(new String(b, "utf-8"));
}
bi.close();
}
@Test
public void testCharArrayReader() throws IOException {
char[] ca = new char[] { 'i', 'a', 'm', 'o', 'k' };
CharArrayReader car = new CharArrayReader(ca);
if (car.ready()) {
char[] cbuf = new char[8];
car.read(cbuf);
System.out.println(cbuf);
}
car.close();
}
@Test
public void testBufferedReader() throws IOException {
BufferedReader br = null;
// br = new BufferedReader(new FileReader(file));
br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8"));
char[] b = new char[(int) file.length()];
br.read(b);
br.close();
System.out.println(new String(b));
}
/**
* write
*/
static File nfile = new File("D://textw.txt");
@Test
public void testOutputStream() throws IOException {
OutputStream os = new FileOutputStream(nfile);
os.write("hellohelloworld".getBytes());
os.close();
read(nfile);
}
private static void read(File f) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(nfile));
String tmp;
while ((tmp = br.readLine()) != null) {
System.out.println(tmp);
}
br.close();
}
@Test
public void testByteArrayOut() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write("what".getBytes());
bos.close();
Assert.assertTrue(bos.size() == 4);
System.out.println(new String(bos.toByteArray()));
Assert.assertTrue("what".equals(bos.toString("utf-8")));
}
@Test
public void testBufferedOut() throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(nfile));
bos.write("hhhhhhhhhhhhhhhhhh".getBytes());
bos.close();
read(nfile);
}
@Test
public void testPrintStream() throws IOException {
PrintStream ps = null;
ps = System.out;
ps.print("sssss");
// ps.close(); 这边不能把ps关了 关了的话就是把System.out关闭了,就不能在控制台打印了
PrintStream os = System.out;
os.println("what");
// ps = new PrintStream(new FileOutputStream(nfile));
// ps.write("aaaa".getBytes());
// ps.write("bbbb".getBytes(), 0, 4);
// ps.println("cccc");
// ps.close();
read(nfile);
}
}
Java NIO和IO的主要区别
NIO即NonBlcoking IO,这个库是在JDK1.4中引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
IO | NIO |
---|---|
面向流 | 面向缓冲(面向块) |
阻塞IO | 非阻塞IO |
无 | 选择器 |
NIO的核心是非阻塞特性和选择器,这是和传统IO最大的区别,传统的阻塞IO操作在发出IO请求后,线程无响应直到IO完成返回,对于类似web服务器的网络IO,不得不采用多线程去维护每一个请求。
面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
阻塞和非阻塞
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
这种特性实际上利用的是IO多路复用,在底层依赖系统函数select或epoll。