Java NIO

从JDK 4开始,Java提供了另一套I/O系统,称为NIO。NIO支持面向缓冲区基于通道的I/O操作。随着JDK 7的发布,Java对NIO系统进行了极大扩展,增强了对文件处理和文件系统特性的支持,因此通常我们将修改后的NIO称为NIO.2。需要强调的是,NIO系统并非用于替换java.io中基于流的I/O类。

1. NIO类

包含NIO类的包如下表所示:
Java NIO_第1张图片

2.缓冲区(Buffers)和通道(Channels)

NIO系统是构建在缓冲区和通道之上的。缓冲器用于容纳数据,通道表示打开到的I/O设备(例如文件或套接字)的连接。然后操作缓冲区,根据需要输入或输出数据。

2.1 缓冲区

要点
缓冲区是包在一个对象内的基本数据元素数组。Buffer类相比一个简单数组的优点是它关于数据的数据内容和信息包含在一个单一的对象中。

缓冲区是在java.nio包中定义的。所有的缓冲区都是Buffer类的子类,Buffer类定义了对所有缓冲区都通用的核心功能:当前位置(current position),界限(limit)和容量(capacity)。当前位置是缓冲区中下一次发生读取和写入操作的索引,当前位置通过大多数读或写操作向前推进。界限是缓冲区中最后一个有效位置之后下一个位置的索引值。容量是缓冲区能够容纳的元素的数量。通常界限等于缓冲区的容量。Buffer类还支持标记(mark)和重置(reset),Buffer类定义了一些方法如下表所示:
Java NIO_第2张图片Java NIO_第3张图片
下面这些特定的缓冲区类派生自Buffer,这些类的名称暗含了它们所能容纳的数据类型:
图 1
Java NIO_第4张图片
MappedByteBuffer是ByteBuffer的子类,用于将文件映射到缓冲区。

下面还是用一点篇幅来介绍这四个缓冲区的属性:
(1) 容量(Capacity)
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定并且永远不能被改变
(2) 上界(Limit)
缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
(3) 位置(Position)
下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
(4) 标记(Mark)
一个备忘位置。调用 mark( )来设定 mark = position。调用 reset( )设定 position = mark。标记在设定前是未定义的(undefined)。
这四个属性之间总是遵循以下关系:
0 <= mark <= position <= limit <= capacity

让我们来看看这些属性在实际应用中的一些例子。下图展示了一个新创建的容量为 10的 ByteBuffer 逻辑视图。
图 2
Java NIO_第5张图片
位置被设为 0,而且容量和上界被设为 10,刚好经过缓冲区能够容纳的最后一个字节。标记最初未定义。容量是固定的,但另外的三个属性可以在使用缓冲区时改变。

在此,需要对上面提到的一些函数作重点说明:

填充—– put()函数

让我们看一个例子。 我们将代表“Hello”字符串的 ASCII 码载入一个名为 buffer 的
ByteBuffer 对象中。当在图 1所新建的缓冲区上执行以下代码后,缓冲区的结果状态如图3示:

buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');

图 3
Java NIO_第6张图片
注意本例中的每个字符都必须被强制转换为 byte。我们不能不经强制转换而这样操做:
buffer.put(‘H’);
因为我们存放的是字节而不是字符。记住在 java 中,字符在内部以 Unicode 码表示,每个 Unicode 字符占 16 位。本节的例子使用包含 ASCII值的字节。通过将char 强制转换为 byte,我们删除了前八位来建立一个八位字节值。这通常只适合于拉丁字符而不能适合所有可能的 Unicode 字符。为了让事情简化,我们暂时故意忽略字符集的映射问题。
既然我们已经在 buffer 中存放了一些数据,如果我们想在不丢失位置的情况下进行一些更改该怎么办呢? put()的绝对方案可以达到这样的目的。假设我们想将缓冲区中的内容从“Hello”的 ASCII 码更改为“Mellow”。我们可以这样实现:

buffer.put(0,(byte)'M').put((byte)'w');

这里通过进行一次绝对方案的 put 将 0 位置的字节代替为十六进制数值 0x4d,将 0x77放入当前位置(当前位置不会受到绝对 put()的影响)的字节,并将位置属性加一。结果如图4所示:
图 4:
Java NIO_第7张图片

翻转 —- flip()函数

我们已经写满了缓冲区,现在我们必须准备将其清空。我们想把这个缓冲区传递给一个通道,以使内容能被全部写出。但如果通道现在在缓冲区上执行 get(),那么它将从我们刚刚插入的有用数据之外取出未定义数据。如果我们将位置值重新设为 0,通道就会从正确位置开始获取,但是它是怎样知道何时到达我们所插入数据末端的呢?这就是上界属性被引入的目的。上界属性指明了缓冲区有效内容的末端。我们需要将上界属性设置为当前位置,然后将位置重置为 0。我们可以人工用下面的代码实现:
buffer.limit(buffer.position()).position(0);

但这种从填充到释放状态的缓冲区翻转是 API 设计者预先设计好的,他们为我们提供了一个非常便利的函数:
Buffer.flip();
Flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态。在翻转之后,图4的缓冲区会变成图5的样子。
图 5
Java NIO_第8张图片

Rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。您可以使用 rewind()后退,重读已经被翻转的缓冲区中的数据

如果将缓冲区翻转两次会怎样呢?它实际上会大小变为 0。按照图 2.5 的相同步骤对缓冲区进行操作;把上界设为位置的值,并把位置设为 0。上界和位置都变成 0。尝试对缓冲区上位置和上界都为 0 的 get()操作会导致 BufferUnderflowException 异常。而 put()则会导致 BufferOverflowException 异常

释放(Draining)

如果我们现在将图5中的缓冲区传入通道,它将取出我们存放在那里的数据,从位置开始直到上界结束。

同样地,如果您接收到一个在别处被填满的缓冲区,您可能需要在检索内容之前将其翻转。例如,如果一个通道的 read()操作完成,而您想要查看被通道放入缓冲区内的数据,那么您需要在调用 get()之前翻转缓冲区。通道对象在缓冲区上调用 put()增加数据; put和read 可以随意混合使用。

布尔函数 hasRemaining()会在释放缓冲区时告诉您是否已经达到缓冲区的上界。以下是一种将数据元素从缓冲区释放到一个数组的方法

for (int i = 0; buffer.hasRemaining( ), i++) {
myByteArray [i] = buffer.get( );
}

作为选择, remaining()函数将告知您从当前位置到上界还剩余的元素数目。 您也可以通过下面的循环来释放图5所示的缓冲区。

int count = buffer.remaining( );
for (int i = 0; i < count, i++) {
myByteArray [i] = buffer.get( );
}

如果您对缓冲区有专门的控制,这种方法会更高效,因为上界不会在每次循环重复时都被检查(要求调用一个 buffer 样例程序)。

一旦缓冲区对象完成填充并释放,它就可以被重新使用了。 Clear()函数将缓冲区重置为空状态。 它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回 0,这使得缓冲区可以被重新填入。

注意
缓冲区并不是多线程安全的。如果您想以多线程同时存取特定的缓冲区,您需要在存取缓冲区之前进行同步(例如对缓冲区对象进行跟踪)。
现在用一个程序展示上面提到的函数的用法:

import java.nio.CharBuffer;

public class BufferFillDrain {
    private static int index = 0;

    private static String [] strings = {
            "A random string value",
            "The product of an infinite number of monkeys",
            "Hey hey we're the Monkees",
            "Opening act for the Monkees: Jimi Hendrix",
            "'Scuse me while I kiss this fly",
            "Help Me! Help Me!",
    };

    public static void main(String[] args) {
        CharBuffer buffer = CharBuffer.allocate(100);

        while (fillBuffer(buffer)) {
            buffer.flip();
            drainBuffer(buffer);
            buffer.clear();
        }
    }

    private static boolean fillBuffer(CharBuffer buffer) {
        if (index >= strings.length) {
            return false;
        }
        String string = strings[index++];

        for (int i = 0; i < string.length(); i++) {
            buffer.put(string.charAt(i));
        }

        return true;
    }

    private static void drainBuffer(CharBuffer buffer) {
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }
        System.out.println("");
    }
}

压缩(Compacting)

public abstract class ByteBuffer
         extends Buffer implements Comparable {
// This is a partial API listing
public abstract ByteBuffer compact( );
}

有时,您可能只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这一点,未读的数据元素需要下移以使第一个元素索引为 0。尽管重复这样做会效率低下,但这有时非常必要,而 API 对此为您提供了一个 compact()函数。这一缓冲区工具在复制数据时要比您使用 get()和 put()函数高效得多。所以当您需要时,请使用 compact()。图 6 显示了一个我们已经释放了一些元素,并且现在我们想要对其进行压缩的缓冲区。
图6
Java NIO_第9张图片
这样操作:
buffer.compact();
会导致缓冲区的状态如图 7 所示:
图7
Java NIO_第10张图片
这里发生了几件事。您会看到数据元素 2-5 被复制到 0-3 位置。位置 4 和 5 不受影响,但现在正在或已经超出了当前位置,因此是“死的”。它们可以被之后的 put()调用重写。还要注意的是,位置已经被设为被复制的数据元素的数目。也就是说,缓冲区现在被定位在缓冲区中最后一个“存活”元素后插入数据的位置。最后,上界属性被设置为容量的值,因此缓冲区可以被再次填满。调用 compact()的作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪。

您可以用这种类似于先入先出(FIFO)队列的方式使用缓冲区。当然也存在更高效的算法(缓冲区移位并不是一个处理队列的非常高效的方法)。但是压缩对于使缓冲区与您从端口中读入的数据(包)逻辑块流的同步来说也许是一种便利的方法。

如果您想在压缩后释放数据,缓冲区会像之前所讨论的那样需要被翻转。无论您之后是否要向缓冲区中添加新的数据,这一点都是必要的。下面通过程序演示上面过程:

import java.nio.CharBuffer;

public class CompactDemo {
    private static int index = 0;
    private static String str = "Mellow";

    public static void main(String[] args) {
        CharBuffer buffer = CharBuffer.allocate(10);
        for(int i = 0; i < str.length(); i++) {
            buffer.put(str.charAt(i));
        }
        buffer.flip();
        for(int i = 0; i < 2; i++) {
            System.out.print(buffer.get()); //释放两个字符
        }
        System.out.println("\nPosition is " + buffer.position());

        buffer.compact();
        buffer.flip();
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }
    }
}

输出结果如下:
Me
Position is 2
llow

标记(Marking)

要点:
使缓冲区能够记住一个位置并在之后将其返回。
缓冲区的标记在 mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。 reset( )函数将位置设为当前的标记值。如果标记值未定义,调用 reset( )将导致 InvalidMarkException 异常。一些缓冲区函数会抛弃已经设定的标记(rewind( ), clear( ),以及 flip( )总是抛弃标记)。如果新设定的值比当前的标记小,调用limit( )或 position( )带有索引参数的版本会抛弃标记。
让我们看看这是如何进行的。在图 5 的缓冲区上执行以下代码将会导致图 8 所显示的缓冲区状态。
buffer.position(2).mark().position(4);
图8:
Java NIO_第11张图片
如果这个缓冲区现在被传递给一个通道,两个字节(“ow”)将会被发送,而位置会前进到 6。如果我们此时调用 reset( ),位置将会被设为标记,如图 9 所示。再次将缓冲区传递给通道将导致四个字节(“llow”)被发送。
图9:
Java NIO_第12张图片

比较(Comparing)

有时候比较两个缓冲区所包含的数据是很有必要的。所有的缓冲区都提供了一个常规的equals( )函数用以测试两个缓冲区的是否相等,以及一个 compareTo( )函数用以比较缓冲区。

public abstract class ByteBuffer extends Buffer implements Comparable {
    // This is a partial API listing
    public boolean equals (Object ob)
    public int compareTo (Object ob)
}
//两个缓冲区可用下面的代码来测试是否相等:
if (buffer1.equals (buffer2)) {
    doSomething( );
}

如果每个缓冲区中剩余的内容相同,那么 equals( )函数将返回 true,否则返回 false。因为这个测试是用于严格的相等而且是可换向的。前面的程序清单中的缓冲区名称可以颠倒,并会产生相同的结果

两个缓冲区被认为相等的充要条件是:
1. 两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer绝不会等于非buffer对象。

2. 两个对象都剩余同样数量的元素。 Buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相同
3. 在每个缓冲区中应被 Get()函数返回的剩余数据元素的每个元素必须相等,而且顺序也要相同。
如果不满足以上任意条件, 就会返回 false。
图 10 说明了两个属性不同的缓冲区也可以相等。
图10:
Java NIO_第13张图片
下面通过一段程序说明:

import java.nio.CharBuffer;

public class CompareDemo {
    private static int index = 0;
    private static String s1 = "dotcom job";
    private static String s2 = "com3dy";
    private static String s3 = "cmo3dy";

    public static void main(String[] args) {
        CharBuffer buffer1 = CharBuffer.allocate(20);
        CharBuffer buffer2 = CharBuffer.allocate(20);
        CharBuffer buffer3 = CharBuffer.allocate(20);

        for(int i = 0; i < s1.length(); i++) {
            buffer1.put(s1.charAt(i));
        }
        buffer1.position(3);
        buffer1.limit(6);

        for(int i = 0; i < s2.length(); i++) {
            buffer2.put(s2.charAt(i));
        }
        buffer2.position(0);
        buffer2.limit(3);

        for (int i = 0; i < s3.length(); i++) {
            buffer3.put(s3.charAt(i));
        }

        System.out.print("buffer1 equals buffer2 ? ");
        System.out.println(buffer1.equals(buffer2));
        System.out.print("buffer1 compare to buffer2 = ");
        System.out.println(buffer1.compareTo(buffer2));

        System.out.print("buffer2 equals buffer3 ? ");
        System.out.println(buffer2.equals(buffer3));
        System.out.print("buffer2 compare to buffer3 = ");
        System.out.println(buffer2.compareTo(buffer3));
    }
}

输出结果:
buffer1 equals buffer2 ? true
buffer1 compare to buffer2 = 0
buffer2 equals buffer3 ? false
buffer2 compare to buffer3 = 99

图 11 显示了两个相似的缓冲区,可能看起来是完全相同的缓冲区,但测试时会发现并不相等。
图11:
Java NIO_第14张图片
缓冲区也支持用 compareTo( )函数以词典顺序进行比较。这一函数在缓冲区参数小
于,等于,或者大于引用 compareTo( )的对象实例时,分别返回一个负整数, 0 和正整数。这些就是所有典型的缓冲区所实现的 java.lang.Comparable 接口语义。这意味着缓冲区数组可以通过调用 java.util.Arrays.sort()函数按照它们的内容进行排序。

与 equals( )相似, compareTo( )不允许不同对象间进行比较。但 compareTo( )更为严格:如果您传递一个类型错误的对象,它会抛出 ClassCastException 异常,但 equals( )只会返回false。

比较是针对每个缓冲区内剩余数据进行的,与它们在 equals( )中的方式相同,直到不相等的元素被发现或者到达缓冲区的上界。如果一个缓冲区在不相等元素发现前已经被耗尽,较短的缓冲区被认为是小于较长的缓冲区。不像 equals( ), compareTo( )不可交换:顺序问题。下面通过一段代码演示图10 过程:

批量移动(Bulk Moves)

缓冲区的涉及目的就是为了能够高效传输数据。一次移动一个数据元素,如前面释放(Draining)所示的那个程序并不高效。buffer API 提供了向缓冲区内外批量移动数据元素的函数。

public abstract class CharBuffer
        extends Buffer implements CharSequence, Comparable {
    // This is a partial API listing
    public CharBuffer get (char [] dst)
    public CharBuffer get (char [] dst, int offset, int length)
    public final CharBuffer put (char[] src)
    public CharBuffer put (char [] src, int offset, int length)
    public CharBuffer put (CharBuffer src)
    public final CharBuffer put (String src)
    public CharBuffer put (String src, int start, int end)
}

当您传入一个数组并且没有指定长度,您就相当于要求整个数组被填充。如果缓冲区中的数据不够完全填满数组,您会得到一个BufferUnderflowException 异常。这意味着如果您想将一个小型缓冲区传入一个大型数组,您需要明确地指定缓冲区中剩余的数据长度。上面的第一个例子不会如您第一眼所推出的结论那样,将缓冲区内剩余的数据元素复制到数组的底部。要将一个缓冲区释放到一个大数组中,要这样做:

char [] bigArray = new char [1000];
// Get count of chars remaining in the buffer
int length = buffer.remaining( );
// Buffer is known to contain < 1,000 chars
buffer.get (bigArrray, 0, length);
// Do something useful with the data
processData (bigArray, length);

记住在调用 get( )之前必须查询缓冲区中的元素数量(因为我们需要告知 processData( )被放置在 bigArray 中的字符个数)。调用 get( )会向前移动缓冲区的位置属性,所以之后调用remaining( )会返回 0。 get( )的批量版本返回缓冲区的引用,而不是被传送的数据元素的计数,以减轻级联调用的困难。

另一方面,如果缓冲区存有比数组能容纳的数量更多的数据,您可以重复利用如下文所示的程序块进行读取:

har [] smallArray = new char [10];
while (buffer.hasRemaining( )) {
int length = Math.min (buffer.remaining( ), smallArray.length);
buffer.get (smallArray, 0, length);
processData (smallArray, length);
}

前面提到的所有缓冲区类都提供了不同的get()put()方法,这些方法可以从缓冲区获取数据或将数据放入缓冲区(如果缓冲区是只读的,就不能使用put()操作)。下表显示了ByteBuffer类定义的get()和put()方法。其它缓冲区具有类似的方法。所有缓冲区类都支持用于执行各种缓冲区操作的方法。例如可以使用allocate()方法手动分配缓冲区,使用wrap()方法在缓冲区中封装数组,使用slice()方法创建缓冲区的子序列。
Java NIO_第15张图片
Java NIO_第16张图片
关于这个 API 有一点要注意的是,像 clear()这类函数,您通常应当返回 void,而不
是 Buffer 引用。这些函数将引用返回到它们在(this)上被引用的对象。这是一个允许级联调用(invocation chaining)的类设计方法。级联调用允许这种类型的代码:
buffer.mark( );
buffer.position(5);
buffer.reset( );
被简写为:
buffer.mark().position(5).reset( );
java.nio 中的类被特意地设计为支持级联调用。您可能已经在 StringBuffer 类中看
到了级联调用的使用。

2.2 通道

通道是由java.io.channels包定义的。通道表示到I/O源或目标的打开的连接。通道实现了Channel接口并扩展了Closeable接口和AutoCloseable接口。通过实现AutoCloseable接口,可以使用try-with-resources语句。

获取通道的一种方式是对支持通道的对象调用getChannel()方法。例如,以下I/O类支持getChannel()方法:
这里写图片描述
根据调用getChannel()方法的对象的类型返回特定类型的通道。例如,当对FileInputStream、FileOutputStream或RandomAccessFile对象调用getChannel()方法时,会返回FileChannel类型的通道。当对Socket对象调用getChannel()方法时,会返回SocketChannel类型的通道。

获取通道的另外一种方式是使用java.nio.file包中的Files类定义的静态方法。例如,使用Files类,可以通过newByteChannel()方法获取字节通道。该方法返回一个SeekableByteChannel对象,SeekableByteChannel是FileChannel实现的一个接口。

FileChannel和SocketChannel这类通道支持各种read()和write()方法,使用这些方法可以通过通道执行I/O操作,下表是为FileChannel定义的一些read()和write()方法。
Java NIO_第17张图片
所有通道都支持一些额外的方法通过这些方法可以访问和控制通道。例如,FileChannel支持获取或设置当前位置的方法、在文件通道之间传递信息的方法、获取当前通道大小的方法以及锁定通道的方法等。FileChannel还提供了静态的open()方法,该方法打开文件并返回指向文件的通道。这还提供了获取通道的另外一种方式。FileChannel还提供了map方法,通过该方法可以将文件映射到缓冲区。

你可能感兴趣的:(java,Java,NIO)