浅谈Java IO流

 阅读约 17 分钟

今天主要来回顾一下有关Java IO流的知识,后面还会对NIO进行介绍。

Java IO

定义

Java的IO流是实现输入和输出的基础,可以方便的实现数据的输入和输出操作。

IO流的分类

  • 按照流的流向分,可以分为输入流和输出流;
  • 按照操作单元划分,可以划分为字节流和字符流;
  • 按照流的角色划分为节点流和处理流。

Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

它们都是一些抽象基类,无法直接创建实例。

常用的IO流的用法

在InputStreamhe里面包含如下3个方法:

int read();
从输入流中读取单个字节,返回所读取的字节数据。
int read(byte[] b);
从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数。
int read(byte[] b,int off,int len); 
从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是
从off位置开始,返回实际读取的字节数。

OutputStream和Writer的用法也非常相似,两个流都提供了如下三个方法:

void write(int c); 
将指定的字节/字符输出到输出流中,其中c即可以代表字节,也可以代表字符。
void write(byte[]/char[] buf);
将字节数组/字符数组中的数据输出到指定输出流中。
void write(byte[]/char[] buf, int off,int len ); 
将字节数组/字符数组中从off位置开始,长度为len的字节/字符输出到输出流中。

因为字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数。Writer里面还包含如下两个方法。

void write(String str);
将str字符串里包含的字符输出到指定输出流中。
void write (String str, int off, int len); 
将str字符串里面从off位置开始,长度为len的字符输出到指定输出流中。

IO文件流

下面我们来看看Java中基于IO的文件流的使用方式

  • FileInputStream 是InputStream子类,以FileInputStream 为例进行文件读取
            File f =new File("d:/czy.txt");
            //创建基于文件的输入流
            FileInputStream fis =new FileInputStream(f);
            //创建字节数组,其长度就是文件的长度
            byte[] all =new byte[(int) f.length()];
            //以字节流的形式读取文件所有内容并返回给all数组
            fis.read(all);
            for (byte b : all) {
                System.out.println(b);
            }             
            //每次使用完流,都应该进行关闭
            fis.close();            

FileReader使用方法和上面一样,只不过是把byte改成char而已。

  • FileWriter 是Writer的子类,以FileWriter 为例把字符串写入到文件
            File f = new File("d:/czy.txt");
            // 创建基于文件的Writer
            FileWriter fr = new FileWriter(f)
            // 以字符流的形式把数据写入到文件中
            String data="abcdefg1234567890";
            char[] cs = data.toCharArray();
            fr.write(cs);

NIO

定义

Java NIO 是JDK 1.4之后新出的一套IO接口,NIO中的N可以理解为Non-blocking。
原来的I/O以流的方式处理数据,而NIO以块的方式处理数据。

组成

NIO最重要的组成部分:

  • 通道 Channel
  • 缓冲区 Buffer
  • 选择器 Selector

什么是通道?

Channel是一个对象,可以通过它读取和写入数据。

所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。
最常用的缓冲区类型是 ByteBuffer

读写操作

从文件中读取

第一步是获取通道。我们从 FileInputStream 获取通道:

FileInputStream fin = new FileInputStream( "d:/czy.txt" );
FileChannel fc = fin.getChannel();

下一步是创建缓冲区:

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

最后,需要将数据从通道读到缓冲区中,如下所示:

fc.read( buffer );

allocate() 方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中,在本例中是一个 ByteBuffer。

您还可以将一个现有的数组转换为缓冲区,如下所示:

byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );

写入文件

在 NIO 中写入文件类似于从文件中读取。首先从 FileOutputStream 获取一个通道:

FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();

下一步是创建一个缓冲区并在其中放入一些数据

ByteBuffer buffer = ByteBuffer.allocate(1024);
//从message数组取出数据 
for (int i=0; i<message.length; ++i) {
     buffer.put(message[i]);
}
buffer.flip();

最后一步是写入缓冲区中:

fc.write( buffer );

flip() 方法让缓冲区可以将新读入的数据写入另一个通道,后面我们会详细介绍。

缓冲区内部细节

状态变量

可以用三个值指定缓冲区在任意时刻的状态:

  • position
  • limit
  • capacity

Position

缓冲区实际上就是一个数组。
如果是写入数据,position指定了下一个字节将放到数组的哪一个元素。
如果是输出数据,position`指定下一个字节来自数组的哪一个元素。

Limit

limit变量表明还有多少数据需要取出或者还有多少空间可以放入数据。

position总是小于或者等于limit

Capacity

缓冲区的capacity表明可以储存在缓冲区中的最大数据容量。

limit决不能大于capacity

观察变量

我们首先观察一个新创建的缓冲区。出于本例子的需要,我们假设这个缓冲区的总容量为8个字节。Buffer的状态如下所示:

Buffer state

回想一下 ,limit决不能大于capacity,此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点。

Array

position设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。position设置如下所示:

Position setting

由于capacity不会改变,所以我们在下面的讨论中可以忽略它。

第一次读取

现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从position开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 3,如下所示:

Position increased to 3

limit没有改变。

第二次读取

在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由position所指定的位置上,position因而增加 2:

Position increased by 2

limit没有改变。

flip

现在我们要将数据写到输出通道中。在这之前,我们必须调用flip()方法。这个方法做两件非常重要的事:

  1. 它将limit设置为当前position
  2. 它将position设置为 0。

前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip 之后的缓冲区:

Buffer after the flip

我们现在可以将数据从缓冲区写入通道了。position被设置为 0,这意味着我们得到的下一个字节是第一个字节。limit已被设置为原来的position,这意味着它包括以前读到的所有字节,并且一个字节也不多。

第一次写入

在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得position增加到 4,而limit不变,如下所示:

Position advanced to 4, limit unchanged

第二次写入

我们只剩下一个字节可写了。limit在我们调用flip()时被设置为 5,并且position不能超过limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得position增加到 5,并保持limit不变,如下所示:

Position advanced to 5, limit unchanged

clear

最后一步是调用缓冲区的clear()方法。这个方法重设缓冲区以便接收更多的字节。Clear做两种非常重要的事情:

  1. 它将limit设置为与capacity相同。
  2. 它设置position为 0。

下图显示了在调用clear()后缓冲区的状态:

State of the buffer after clear() has been called

缓冲区现在可以接收新的数据了。

缓冲区的使用

下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。

while (true) {
     buffer.clear();
     int r = fcin.read(buffer);
 
     if (r==-1) {
       break;
     }
 
     buffer.flip();
     fcout.write(buffer);
}

read()write() 调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。
clear()flip() 方法用于让缓冲区在读和写之间切换。

Selectors

主要用于异步I/O

Selector就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

我们需要做的第一件事就是创建一个 Selector

Selector selector = Selector.open();

然后,我们将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。

打开一个 ServerSocketChannel

为了接收连接,我们需要一个 ServerSocketChannel。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口,我们打开一个 ServerSocketChannel,如下所示:

ServerSocketChannel ssc = ServerSocketChannel.op
en();
ssc.configureBlocking( false );//非阻塞
 
ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress(ports[i]);
ss.bind(address);//绑定端口

第一行创建一个新的 ServerSocketChannel ,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。

注册

将新打开的 ServerSocketChannels 注册到 Selector上

SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

内部循环

使用 Selector 的几乎每个程序都像下面这样使用内部循环:

我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生
select() 方法将返回所发生的事件的数量

int num = selector.select();

调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个集合

Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
 
while (it.hasNext()) {
     SelectionKey key = (SelectionKey)it.next();
     // ... deal with I/O event ...
} 

接受新的连接

通过channel()方法可以取得通道对象

ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();

下一步是将新连接的 SocketChannel 配置为非阻塞的

sc.configureBlocking( false );
SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );

关于Java IO和NIO的知识暂且学习到这里,后面有时间会继续补充!

参考

NIO入门

阅读 228更新于 2019-10-25

推荐阅读
目录