[TOC] # 附录:新IO Java “新” I/O 库,是在Java 1.4引入到 **Java .nio.* package** 中。它有一个目标 : 速度。 实际上,“旧的”I/O包是使用 **nio** 重新实现了,因此速度也得到提升,因此即使不显式地使用nio 编写代码,也可以提速。这种速度的提高既发生在文件 I/O 中(这里讨论),也发生在网络I/O 中(例如用于Internet编程)。 速度来自于使用更接近操作系统执行 I/O 方式的结构 : 通道和缓冲区。把它想象成一个煤矿;通道是包含煤层的矿井(数据),缓冲区是您发送到矿井的小车。车里装满了煤,你从车上拿煤。也就是说,你不能直接和通道互动 ; 您与缓冲区交互并将缓冲区发送到通道中。通道要么从缓冲区中提取数据,要么将数据放入缓冲区。 本附录将深入探讨 nio 包。像I/O streams 这样的高级库使用 **nio**,但是大多数时候您不需要在这个级别使用 I/O 。在Java 7和Java 8,您(理想情况下)除了特殊情况外,甚至不需要使用I/O流。理想情况下,您将经常使用的所有内容都包含在文件一章中。只有在处理性能问题 (例如,可能需要内存映射文件)或创建自己的 I/O 库时,才需要理解 **nio**。 ## ByteBuffer 直接与通道通信的缓冲区只有一个类型,就是 bytebuffer 。也就是说,一个保存原始字节的缓冲区。如果您查看 **java.nio.ByteBuffer** 的JDK文档,你会发现它很基本 : 您可以通过告诉它要分配多少存储空间来创建一个方法,并且可以使用一些方法来放置和获取数据,这些方法可以是原始字节形式的数据,也可以是原始数据类型的数据。但是没有办法放置或获取对象,甚至字符串。它是相当低层次的,因为这使得大多数操作系统的映射更加有效。 “旧的” I/O 中的三个类被修改,可以生成一个 **FileChannel** 分别是: **FileInputStream**、**FileOutputStream**,以及用于读写的 **RandomAccessFile**。 注意,这些是字节操作流,符合 **nio** 的底层特性。 字符模式类的 **Reader** 和 **Writer** 不生成通道,但通道类具有从通道生成 **Reader** 和 **Writer** 的实用方法。 在这里,我们使用所有三种类型的流来生成可写、可读/可写和可读的通道: ```java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Getting channels from streams import java.nio.*; import java.nio.channels.*; import java.io.*; public class GetChannel { private static String name = "data.txt"; private static final int BSIZE = 1024; public static void main(String[] args) { // Write a file: try( FileChannel fc = new FileOutputStream(name) .getChannel() ) { fc.write(ByteBuffer .wrap("Some text ".getBytes())); } catch(IOException e) { throw new RuntimeException(e); } // Add to the end of the file: try( FileChannel fc = new RandomAccessFile( name, "rw").getChannel() ) { fc.position(fc.size()); // Move to the end fc.write(ByteBuffer .wrap("Some more".getBytes())); } catch(IOException e) { throw new RuntimeException(e); } // Read the file: try( FileChannel fc = new FileInputStream(name) .getChannel() ) { ByteBuffer buff = ByteBuffer.allocate(BSIZE); fc.read(buff); buff.flip(); while(buff.hasRemaining()) System.out.write(buff.get()); } catch(IOException e) { throw new RuntimeException(e); } System.out.flush(); } } /* Output: Some text Some more */ ``` 对于这里显示的任何流类,**getChannel( )** 将生成一个 **FileChannel** 。通道是相当基本的:给它一个**ByteBuffer** 用于读写,并为独占访问锁定文件的区域(稍后将对此进行描述)。 将字节放入 **ByteBuffer** 的一种方法是直接使用 “put” 方法,将一个或多个字节填入 ByteBuffer,当然也可以是其它原始类型。但是,正如这里所看到的,您还可以使用 **wrap()** 方法在 **ByteBuffer**中“包装”现有字节数组。执行此操作时,不会复制底层数组,而是将其用作生成的 **ByteBuffer** 的存储。这样生产的 **ByteBuffer** 是由数组“支持”的。 data.txt 文件使用RandomAccessFile重新打开。注意,您可以在文件中移动 **FileChanne** l; 在这里,它被移动到末尾,以便添加额外的写操作。 对于只读访问,必须使用 **static allocate()** 方法显式地分配 **ByteBuffer**。**nio** 的目标是快速移动大量数据,因此 **ByteBuffer** 的大小应该很重要——实际上,这里使用的1K可能比您通常使用的要小很多(您必须对工作应用程序进行试验,以找到最佳大小)。 还可以通过使用 **allocateDirect()** 而不是 **allocation()** 来生成一个“直接”缓冲区来获得更快的速度,该缓冲区可以与操作系统进行更高的耦合。然而,这种分配的开销更大,而且实际实现因操作系统的不同而有所不同,因此,您必须再次试验您的工作应用程序,以发现直接缓冲区是否会为您带来速度上的优势。 一旦您调用 **read()** 来告诉 **FileChannel** 将字节存储到 **ByteBuffer** 中,您必须调用缓冲区上的 **flip()**来告诉它准备好提取字节(是的,这看起来有点粗糙,但是请记住,这是非常低级的,而且是为了达到最高速度)。如果我们要为进一步的 **read()** 操作使用缓冲区,我们还将调用 **clear()** 为每个 **read()** 准备缓冲区。这个简单的文件复制程序演示: ```java // newio/ChannelCopy.java // Copying a file using channels and buffers // {java ChannelCopy ChannelCopy.java test.txt} import java.nio.*; import java.nio.channels.*; import java.io.*; public class ChannelCopy { private static final int BSIZE = 1024; public static void main(String[] args) { if(args.length != 2) { System.out.println( "arguments: sourcefile destfile"); System.exit(1); } try( FileChannel in = new FileInputStream( args[0]).getChannel(); FileChannel out = new FileOutputStream( args[1]).getChannel() ) { ByteBuffer buffer = ByteBuffer.allocate(BSIZE); while(in.read(buffer) != -1) { buffer.flip(); // Prepare for writing out.write(buffer); buffer.clear(); // Prepare for reading } } catch(IOException e) { throw new RuntimeException(e); } } } ``` 打开一个 **FileChannel** 用于读取,另一个 **FileChannel** 用于写入。分配了一个 **ByteBuffer**,当**FileChannel.read() **返回 **-1** 时(毫无疑问,这是Unix和C语言中的一个剩余物 (holdover) ),意味着您已经完成了输入。每次 **read()** 将数据放入缓冲区之后,**flip()** 都会准备好缓冲区,以便 **write()** 提取它的信息。写()之后,信息仍然在缓冲区中,**clear()** 重置所有内部指针,以便在另一次 **read()** 期间接受数据。 但是,前面的程序并不是处理这种操作的理想方法。特殊方法 transferTo() 和 transferFrom( )允许你直接连接一个通道到另一个: ```java // newio/TransferTo.java // Using transferTo() between channels // {java TransferTo TransferTo.java TransferTo.txt} import java.nio.channels.*; import java.io.*; public class TransferTo { public static void main(String[] args) { if(args.length != 2) { System.out.println( "arguments: sourcefile destfile"); System.exit(1); } try( FileChannel in = new FileInputStream( args[0]).getChannel(); FileChannel out = new FileOutputStream( args[1]).getChannel() ) { in.transferTo(0, in.size(), out); // Or: // out.transferFrom(in, 0, in.size()); } catch(IOException e) { throw new RuntimeException(e); } } } ``` 你不会经常这样做,但知道这一点很好。 ## 转换数据 在 **GetChannel** 中打印文件中的信息。在 java 中,我们每次提取一个字节的数据,并将每个字节转换为 char。这看起来很简单——如果您查看**java.nio.CharBuffer** 类,您将看到它有一个toString()方法,该方法说,“返回一个包含这个缓冲区中的字符的字符串。” 既然 **ByteBuffer** 可以用 **asCharBuffer()** 方法看作 **CharBuffer**,为什么不使用它呢? 从下面输出语句的第一行可以看出,这并不正确: ```java // newio/BufferToText.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Converting text to and from ByteBuffers import java.nio.*; import java.nio.channels.*; import java.nio.charset.*; import java.io.*; public class BufferToText { private static final int BSIZE = 1024; public static void main(String[] args) { try( FileChannel fc = new FileOutputStream( "data2.txt").getChannel() ) { fc.write(ByteBuffer.wrap("Some text".getBytes())); } catch(IOException e) { throw new RuntimeException(e); } ByteBuffer buff = ByteBuffer.allocate(BSIZE); try( FileChannel fc = new FileInputStream( "data2.txt").getChannel() ) { fc.read(buff); } catch(IOException e) { throw new RuntimeException(e); } buff.flip(); // Doesn't work: System.out.println(buff.asCharBuffer()); // Decode using this system's default Charset: buff.rewind(); String encoding = System.getProperty("file.encoding"); System.out.println("Decoded using " + encoding + ": " + Charset.forName(encoding).decode(buff)); // Encode with something that prints: try( FileChannel fc = new FileOutputStream( "data2.txt").getChannel() ) { fc.write(ByteBuffer.wrap( "Some text".getBytes("UTF-16BE"))); } catch(IOException e) { throw new RuntimeException(e); } // Now try reading again: buff.clear(); try( FileChannel fc = new FileInputStream( "data2.txt").getChannel() ) { fc.read(buff); } catch(IOException e) { throw new RuntimeException(e); } buff.flip(); System.out.println(buff.asCharBuffer()); // Use a CharBuffer to write through: buff = ByteBuffer.allocate(24); buff.asCharBuffer().put("Some text"); try( FileChannel fc = new FileOutputStream( "data2.txt").getChannel() ) { fc.write(buff); } catch(IOException e) { throw new RuntimeException(e); } // Read and display: buff.clear(); try( FileChannel fc = new FileInputStream( "data2.txt").getChannel() ) { fc.read(buff); } catch(IOException e) { throw new RuntimeException(e); } buff.flip(); System.out.println(buff.asCharBuffer()); } } /* Output: ???? Decoded using windows-1252: Some text Some text Some textNULNULNUL */ ``` 缓冲区包含普通字节,为了将这些字节转换为字符,我们必须在输入时对它们进行编码(这样它们输出时就有意义了),或者在输出时对它们进行解码。 这可以使用 **java.nio.charset** 来完成。**Charset** 类,它提供工具来编码成许多不同类型的字符集: ```java // newio/AvailableCharSets.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Displays Charsets and aliases import java.nio.charset.*; import java.util.*; public class AvailableCharSets { public static void main(String[] args) { SortedMap charSets = Charset.availableCharsets(); for(String csName : charSets.keySet()) { System.out.print(csName); Iterator aliases = charSets.get(csName) .aliases().iterator(); if(aliases.hasNext()) System.out.print(": "); while(aliases.hasNext()) { System.out.print(aliases.next()); if(aliases.hasNext()) System.out.print(", "); } System.out.println(); } } } /* Output: (First 7 Lines) Big5: csBig5 Big5-HKSCS: big5-hkscs, big5hk, Big5_HKSCS, big5hkscs CESU-8: CESU8, csCESU-8 EUC-JP: csEUCPkdFmtjapanese, x-euc-jp, eucjis, Extended_UNIX_Code_Packed_Format_for_Japanese, euc_jp, eucjp, x-eucjp EUC-KR: ksc5601-1987, csEUCKR, ksc5601_1987, ksc5601, 5601, euc_kr, ksc_5601, ks_c_5601-1987, euckr GB18030: gb18030-2000 GB2312: gb2312, euc-cn, x-EUC-CN, euccn, EUC_CN, gb2312-80, gb2312-1980 ... */ ``` 回到 **BufferToText** 。java 中,如果您 **rewind()** 缓冲区(回到数据的开头),然后使用该平台的默认字符集 **decode()** 数据,生成的 **CharBuffer** 将在控制台上正常显示。要发现默认字符集,使用**System.getProperty(“file.encoding”)**,它生成命名字符集的字符串。 另一种方法是使用字符集 **encode()**,该字符集在读取文件时生成可打印的内容,如您在**BufferToText.java** 的第三部分中所看到的。这里,**UTF-16BE** 用于将文本写入文件,当读取文本时,您所要做的就是将其转换为 **CharBuffer**,并生成预期的文本。最后,您将看到如果通过**CharBuffer** 写入 **ByteBuffer** 会发生什么(稍后您将对此有更多的了解)。注意,为 **ByteBuffer** 分配了24个字节。 由于每个字符需要两个字节,这对于12个字符已经足够了,但是“some text”只有9个字节。其余的零字节仍然出现在由其toString()生成的CharBuffer的表示中,如输出所示。 ## 获取原始类型 虽然 **ByteBuffer** 只包含字节,但它包含了一些方法,用于从它所包含的字节中生成每种不同类型的基元值。这个例子展示了使用以下方法插入和提取各种值: ```java // newio/GetData.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Getting different representations from a ByteBuffer import java.nio.*; public class GetData { private static final int BSIZE = 1024; public static void main(String[] args) { ByteBuffer bb = ByteBuffer.allocate(BSIZE); // Allocation automatically zeroes the ByteBuffer: int i = 0; while(i++ < bb.limit()) if(bb.get() != 0) System.out.println("nonzero"); System.out.println("i = " + i); bb.rewind(); // Store and read a char array: bb.asCharBuffer().put("Howdy!"); char c; while((c = bb.getChar()) != 0) System.out.print(c + " "); System.out.println(); bb.rewind(); // Store and read a short: bb.asShortBuffer().put((short)471142); System.out.println(bb.getShort()); bb.rewind(); // Store and read an int: bb.asIntBuffer().put(99471142); System.out.println(bb.getInt()); bb.rewind(); // Store and read a long: bb.asLongBuffer().put(99471142); System.out.println(bb.getLong()); bb.rewind(); // Store and read a float: bb.asFloatBuffer().put(99471142); System.out.println(bb.getFloat()); bb.rewind(); // Store and read a double: bb.asDoubleBuffer().put(99471142); System.out.println(bb.getDouble()); bb.rewind(); } } /* Output: i = 1025 H o w d y ! 12390 99471142 99471142 9.9471144E7 9.9471142E7 */ ``` 在分配 **ByteBuffer** 之后,将检查它的值,以确定缓冲区分配是否会自动将内容归零——它确实会这样做。检查所有1,024个值(直到 position的值为 缓冲区的 **limit()** ),所有值都为零。 将原始值插入ByteBuffer的最简单方法是使用 **asCharBuffer()** 、**asShortBuffer()** 等获取该缓冲区的适当“视图”,然后使用该视图的 **put()** 方法。 这将对每个基本数据类型执行。其中唯一有点奇怪的是 **ShortBuffer** 的 **put()**,它需要强制转换 (强制转换并更改结果值)。所有其他视图缓冲区都不需要在它们的 **put()** 方法中强制转换。 ## 视图缓冲区 “视图缓冲区”通过特定原始类型的窗口来查看底层 **ByteBuffer**。**ByteBuffer** 仍然是“支持”视图的实际存储,因此对视图所做的任何更改都反映在对 **ByteBuffer** 中的数据的修改中。 如前面的示例所示,这方便地将基本类型插入 **ByteBuffer**。视图缓冲区还可以从 **ByteBuffer** 读取原始值,一次读取一个(ByteBuffer允许),或者批量读取(数组)。下面是一个通过 **IntBuffer**在**ByteBuffer** 中操作 int 的例子: ```java // newio/IntBufferDemo.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Manipulating ints in a ByteBuffer with an IntBuffer import java.nio.*; public class IntBufferDemo { private static final int BSIZE = 1024; public static void main(String[] args) { ByteBuffer bb = ByteBuffer.allocate(BSIZE); IntBuffer ib = bb.asIntBuffer(); // Store an array of int: ib.put(new int[]{ 11, 42, 47, 99, 143, 811, 1016 }); // Absolute location read and write: System.out.println(ib.get(3)); ib.put(3, 1811); // Setting a new limit before rewinding the buffer. ib.flip(); while(ib.hasRemaining()) { int i = ib.get(); System.out.println(i); } } } /* Output: 99 11 42 47 1811 143 811 1016 */ ``` 重载的 **put()** 方法首先用于存储 **int** 数组。下面的 **get()** 和 **put()** 方法调用直接访问底层 **ByteBuffer** 中的 **int** 位置。请注意,通过直接操作 **ByteBuffer** ,这些绝对位置访问也可以用于基本类型。 一旦底层 **ByteBuffer** 通过视图缓冲区填充了 **int** 或其他基本类型,那么就可以直接将该 **ByteBuffer **写入通道。您可以轻松地从通道读取数据,并使用视图缓冲区将所有内容转换为特定类型的原语。下面是一个例子,通过在同一个 **ByteBuffer** 上生成不同的视图缓冲区,将相同的字节序列解释为 **short**、**int**、**float**、**long **和 **double**: ```java // newio/ViewBuffers.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. import java.nio.*; public class ViewBuffers { public static void main(String[] args) { ByteBuffer bb = ByteBuffer.wrap( new byte[]{ 0, 0, 0, 0, 0, 0, 0, 'a' }); bb.rewind(); System.out.print("Byte Buffer "); while(bb.hasRemaining()) System.out.print( bb.position()+ " -> " + bb.get() + ", "); System.out.println(); CharBuffer cb = ((ByteBuffer)bb.rewind()).asCharBuffer(); System.out.print("Char Buffer "); while(cb.hasRemaining()) System.out.print( cb.position() + " -> " + cb.get() + ", "); System.out.println(); FloatBuffer fb = ((ByteBuffer)bb.rewind()).asFloatBuffer(); System.out.print("Float Buffer "); while(fb.hasRemaining()) System.out.print( fb.position()+ " -> " + fb.get() + ", "); System.out.println(); IntBuffer ib = ((ByteBuffer)bb.rewind()).asIntBuffer(); System.out.print("Int Buffer "); while(ib.hasRemaining()) System.out.print( ib.position()+ " -> " + ib.get() + ", "); System.out.println(); LongBuffer lb = ((ByteBuffer)bb.rewind()).asLongBuffer(); System.out.print("Long Buffer "); while(lb.hasRemaining()) System.out.print( lb.position()+ " -> " + lb.get() + ", "); System.out.println(); ShortBuffer sb = ((ByteBuffer)bb.rewind()).asShortBuffer(); System.out.print("Short Buffer "); while(sb.hasRemaining()) System.out.print( sb.position()+ " -> " + sb.get() + ", "); System.out.println(); DoubleBuffer db = ((ByteBuffer)bb.rewind()).asDoubleBuffer(); System.out.print("Double Buffer "); while(db.hasRemaining()) System.out.print( db.position()+ " -> " + db.get() + ", "); } } /* Output: Byte Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 0, 4 -> 0, 5 -> 0, 6 -> 0, 7 -> 97, Char Buffer 0 -> NUL, 1 -> NUL, 2 -> NUL, 3 -> a, Float Buffer 0 -> 0.0, 1 -> 1.36E-43, Int Buffer 0 -> 0, 1 -> 97, Long Buffer 0 -> 97, Short Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 97, Double Buffer 0 -> 4.8E-322, */ ``` **ByteBuffer** 通过“包装”一个8字节数组生成,然后通过所有不同基本类型的视图缓冲区显示该数组。下图显示了从不同类型的缓冲区读取数据时,数据显示的差异: ![image-20190324153222402](/Users/langdon/Library/Application Support/typora-user-images/image-20190324153222402.png) ### 端 不同的机器可以使用不同的字节顺序方法来存储数据。“Big endian”将最重要的字节(the most significant byte)放在最低内存地址中,而“little endian”将最重要的字节放在最高内存地址中。 当存储一个大于一个字节的量时,例如 **int**、**float** 等,您可能需要考虑字节排序。字节缓冲区以大端字节形式存储数据,通过网络发送的数据总是使用大端字节顺序。您可以使用 **order()** 和**ByteOrder** 参数来更改 **ByteBuffer** 端,可选参数只有2个:**ByteOrder.BIG_ENDIAN** 或 **ByteOrder.LITTLE_ENDIAN**。 考虑一个包含以下两个字节的 **ByteBuffer** :将数据作为一个**short** (**ByteBuffer. asshortbuffer()**) 读取,生成数字 97 (00000000 01100001)。更改为 little endian 将生成数字 24832 (01100001 00000000)。 这显示了字节顺序的变化取决于 endian 设置: ```java // newio/Endians.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Endian differences and data storage import java.nio.*; import java.util.*; public class Endians { public static void main(String[] args) { ByteBuffer bb = ByteBuffer.wrap(new byte[12]); bb.asCharBuffer().put("abcdef"); System.out.println(Arrays.toString(bb.array())); bb.rewind(); bb.order(ByteOrder.BIG_ENDIAN); bb.asCharBuffer().put("abcdef"); System.out.println(Arrays.toString(bb.array())); bb.rewind(); bb.order(ByteOrder.LITTLE_ENDIAN); bb.asCharBuffer().put("abcdef"); System.out.println(Arrays.toString(bb.array())); } } /* Output: [0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102] [0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102] [97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0] */ ``` **ByteBuffer** 分配空间将 **charArray** 中的所有字节作为外部缓冲区保存,因此可以调用 **array()** 方法来显示底层字节。**array()** 方法是“可选的”,您只能在数组支持的缓冲区上调用它 ,否则您将得到一个 **UnsupportedOperationException**。 **charArray** 通过 **CharBuffer** 视图插入到 **ByteBuffer **中。当显示底层字节时,默认顺序与随后的大端序相同,而小端序则交换字节。 ## 使用缓冲区进行数据操作 下图说明了 nio 类之间的关系,展示了如何移动和转换数据。例如,要将字节数组写入文件,使用ByteBuffer.wrap() 方法包装字节数组,使用 **getChannel()** 在 **FileOutputStream** 上打开通道,然后从 **ByteBuffer** 将数据写入 **FileChannel**。 ![image-20190324153202297](/Users/langdon/Library/Application Support/typora-user-images/image-20190324153202297.png) **ByteBuffer** 是将数据移入和移出通道的唯一方法,您只能创建一个独立的原始类型的缓冲区,或者使用“as”方法从ByteBuffer获得一个新缓冲区。也就是说,不能将基元类型的缓冲区转换为ByteBuffer。但您能够通过视图缓冲区将原始数据移动到 **ByteBuffer** 中或移出 **ByteBuffer**。 ### 缓冲区的细节 缓冲区由数据和四个索引组成,以有效地访问和操作该数据: 标记、位置、限制 和 容量(mark, position, limit and capacity)。有一些方法可以设置和重置这些索引并查询它们的值。 **capacity()** 返回缓冲区的 capacity。 **clear()** 清除缓冲区,将 position 设置为零并 设 limit 为 capacity。您可以调用此方法来覆盖现有缓冲区。 **flip()** 将 limit 设置为位置,并将 position 设置为零。此方法用于准备缓冲区,以便在数据写入缓冲区后进行读取。 **limit()** 返回 limit 的值。 **limit(int lim)** 重设 limit **mark()** 设置 mark 为当前的 position **position()** 返回 position **position(int pos) ** 设置 position **remaining()** 返回 limit - position 。 **hasRemaining()** 如果在 position 与 limit 中间有元素,返回 **true** 从缓冲区插入和提取数据的方法更新这些索引来反映所做的更改。这个例子使用了一个非常简单的算法(交换相邻的字符)来打乱和整理字符在CharBuffer: ```java // newio/UsingBuffers.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. import java.nio.*; public class UsingBuffers { private static void symmetricScramble(CharBuffer buffer) { while(buffer.hasRemaining()) { buffer.mark(); char c1 = buffer.get(); char c2 = buffer.get(); buffer.reset(); buffer.put(c2).put(c1); } } public static void main(String[] args) { char[] data = "UsingBuffers".toCharArray(); ByteBuffer bb = ByteBuffer.allocate(data.length * 2); CharBuffer cb = bb.asCharBuffer(); cb.put(data); System.out.println(cb.rewind()); symmetricScramble(cb); System.out.println(cb.rewind()); symmetricScramble(cb); System.out.println(cb.rewind()); } } /* Output: UsingBuffers sUniBgfuefsr UsingBuffers */ ``` 虽然可以通过使用 **char** 数组调用 **wrap()** 直接生成 **CharBuffer**,但是底层的 **ByteBuffer** 将被分配,而 **CharBuffer** 将作为 **ByteBuffer** 上的视图生成。这强调了目标始终是操作 **ByteBuffer**,因为它与通道交互。 下面是程序在 **symmetricgrab()** 方法入口时缓冲区的样子: ![image-20190324155153600](/Users/langdon/Library/Application Support/typora-user-images/image-20190324155153600.png) position 指向缓冲区中的第一个元素,capacity 和 limie 紧接在最后一个元素之后。在**symmetricgrab()** 中,while循环迭代到 position 等于 limit。当在缓冲区上调用相对位置的 get() 或 put() 函数时,缓冲区的位置会发生变化。您还可以调用绝对位置的 get() 和 put() 方法,它们包含索引参数 : get()或put()发生的位置。这些方法不修改缓冲区 position 的值。 当控件进入while循环时,使用 **mark()** 调用设置 mark 的值。缓冲区的状态为: 两个相对 get() 调用将前两个字符的值保存在变量c1和c2中。在这两个调用之后,缓冲区看起来是这样的 : 为了执行交换,我们在位置0处编写c2,在位置1处编写c1。我们可以使用绝对put方法来实现这一点,或者用 reset() 方法,将 position 的值设置为 mark 。 两个put()方法分别编写c2和c1 : 在循环的下一次迭代中,将mark设置为position的当前值 : 该过程将继续,直到遍历整个缓冲区为止。在while循环的末尾,position位于缓冲区的末尾。如果显示缓冲区,则只显示位置和限制之间的字符。因此,要显示缓冲区的全部内容,必须使用 rewind() 将位置设置为缓冲区的开始位置。这是 rewind() 调用后 buffer 的状态(mark的值变成undefined): ![image-20190324155528149](/Users/langdon/Library/Application Support/typora-user-images/image-20190324155528149.png) 再次调用 **symmetricgrab()** 函数时,**CharBuffer** 将经历相同的过程并恢复到原始状态。 ## 内存映射文件 内存映射文件允许您创建和修改太大而无法放入内存的文件。使用内存映射文件,您可以假装整个文件都在内存中,并将其视为一个非常大的数组来访问它。这种方法大大简化了您编写的修改文件的代码: ```java // newio/LargeMappedFiles.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Creating a very large file using mapping import java.nio.*; import java.nio.channels.*; import java.io.*; public class LargeMappedFiles { static int length = 0x8000000; // 128 MB public static void main(String[] args) throws Exception { try( RandomAccessFile tdat = new RandomAccessFile("test.dat", "rw") ) { MappedByteBuffer out = tdat.getChannel().map( FileChannel.MapMode.READ_WRITE, 0, length); for(int i = 0; i < length; i++) out.put((byte)'x'); System.out.println("Finished writing"); for(int i = length/2; i < length/2 + 6; i++) System.out.print((char)out.get(i)); } } } /* Output: Finished writing xxxxxx */ ``` 为了读写,我们从 **RandomAccessFile** 开始,获取该文件的通道,然后调用 **map()** 来生成**MappedByteBuffer** ,这是一种特殊的直接缓冲区。您必须指定要在文件中映射的区域的起始点和长度—这意味着您可以选择映射大文件的较小区域。**MappedByteBuffer** 继承了它的**ByteBuffer**,所以它有所有的 **ByteBuffer **方法。这里只展示了 **put()** 和 **get()** 的最简单用法,但是您也可以使用 **asCharBuffer()** 等方法。 使用前面的程序创建的文件有128mb长,可能比您的操作系统一次允许的内存要大。该文件似乎可以同时访问,因为它只有一部分被带进内存,而其他部分被交换出去。这样,一个非常大的文件(最多2gb)可以很容易地修改。注意,底层操作系统的文件映射工具用于最大化性能。 ### 性能 虽然“旧”流I/O的性能通过使用 **nio** 实现得到了改进,但是映射文件访问往往要快得多。这个程序做一个简单的性能比较: ```java // newio/MappedIO.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. import java.util.*; import java.nio.*; import java.nio.channels.*; import java.io.*; public class MappedIO { private static int numOfInts = 4_000_000; private static int numOfUbuffInts = 100_000; private abstract static class Tester { private String name; Tester(String name) { this.name = name; } public void runTest() { System.out.print(name + ": "); long start = System.nanoTime(); test(); double duration = System.nanoTime() - start; System.out.format("%.3f%n", duration/1.0e9); } public abstract void test(); } private static Tester[] tests = { new Tester("Stream Write") { @Override public void test() { try( DataOutputStream dos = new DataOutputStream( new BufferedOutputStream( new FileOutputStream( new File("temp.tmp")))) ) { for(int i = 0; i < numOfInts; i++) dos.writeInt(i); } catch(IOException e) { throw new RuntimeException(e); } } }, new Tester("Mapped Write") { @Override public void test() { try( FileChannel fc = new RandomAccessFile("temp.tmp", "rw") .getChannel() ) { IntBuffer ib = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size()).asIntBuffer(); for(int i = 0; i < numOfInts; i++) ib.put(i); } catch(IOException e) { throw new RuntimeException(e); } } }, new Tester("Stream Read") { @Override public void test() { try( DataInputStream dis = new DataInputStream( new BufferedInputStream( new FileInputStream("temp.tmp"))) ) { for(int i = 0; i < numOfInts; i++) dis.readInt(); } catch(IOException e) { throw new RuntimeException(e); } } }, new Tester("Mapped Read") { @Override public void test() { try( FileChannel fc = new FileInputStream( new File("temp.tmp")).getChannel() ) { IntBuffer ib = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()).asIntBuffer(); while(ib.hasRemaining()) ib.get(); } catch(IOException e) { throw new RuntimeException(e); } } }, new Tester("Stream Read/Write") { @Override public void test() { try( RandomAccessFile raf = new RandomAccessFile( new File("temp.tmp"), "rw") ) { raf.writeInt(1); for(int i = 0; i < numOfUbuffInts; i++) { raf.seek(raf.length() - 4); raf.writeInt(raf.readInt()); } } catch(IOException e) { throw new RuntimeException(e); } } }, new Tester("Mapped Read/Write") { @Override public void test() { try( FileChannel fc = new RandomAccessFile( new File("temp.tmp"), "rw").getChannel() ) { IntBuffer ib = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size()).asIntBuffer(); ib.put(0); for(int i = 1; i < numOfUbuffInts; i++) ib.put(ib.get(i - 1)); } catch(IOException e) { throw new RuntimeException(e); } } } }; public static void main(String[] args) { Arrays.stream(tests).forEach(Tester::runTest); } } /* Output: Stream Write: 0.615 Mapped Write: 0.050 Stream Read: 0.577 Mapped Read: 0.015 Stream Read/Write: 4.069 Mapped Read/Write: 0.013 */ ``` **Tester** 是一个模板方法模式,它为匿名内部子类中定义的 **test()** 的各种实现创建一个测试框架。每个子类都执行一种测试,因此 test() 方法还为您提供了执行各种I/O活动的原型。 虽然映射的写似乎使用 **FileOutputStream**,但是文件映射中的所有输出必须使用 **RandomAccessFile**,就像前面代码中的读/写一样。 请注意,**test()** 方法包括初始化各种I/O对象的时间,因此,尽管映射文件的设置可能很昂贵,但是与流I/O相比,总体收益非常可观。 ## 文件锁定 文件锁定同步访问,因此文件可以是共享资源。但是,争用同一个文件的两个线程可能位于不同的jvm 中,可能一个是 Java 线程,另一个是操作系统中的某个本机线程。文件锁定对其他操作系统进程是可见的,因为Java文件锁定直接映射到本机操作系统锁定工具。 ```java // newio/FileLocking.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. import java.nio.channels.*; import java.util.concurrent.*; import java.io.*; public class FileLocking { public static void main(String[] args) { try( FileOutputStream fos = new FileOutputStream("file.txt"); FileLock fl = fos.getChannel().tryLock() ) { if(fl != null) { System.out.println("Locked File"); TimeUnit.MILLISECONDS.sleep(100); fl.release(); System.out.println("Released Lock"); } } catch(IOException | InterruptedException e) { throw new RuntimeException(e); } } } /* Output: Locked File Released Lock */ ``` 通过调用 **FileChannel** 上的 **tryLock()** 或 **lock()**,可以获得整个文件的 **FileLock**。( **SocketChannel、DatagramChannel **和 **ServerSocketChannel **不需要锁定,因为它们本质上是单进程实体 ; 通常不会在两个进程之间共享一个网络套接字) **tryLock()** 是非阻塞的。它试图抓住锁,但如果它不能抓住了(当其他进程已经持有相同的锁,并且它不是共享的),它只是从方法调用返回。 **lock()** 会阻塞,直到获得锁,或者调用 **lock()** 的线程中断,或者调用 **lock()** 方法的通道关闭。使 用**FileLock.release()** 释放锁。还可以使用 **tryLock(long position, long size, boolean shared)** 或**lock(long position, long size, boolean shared)** 锁定文件的一部分,锁住该区域(size-position)。第三个参数指定是否共享此锁。 虽然零参数锁定方法适应文件大小的变化,但是如果文件大小发生变化,具有固定大小的锁不会发生变化。如果从一个位置到另一个位置获得一个锁,并且文件的增长超过了 position + size ,那么超出位置+大小的部分没有被锁定。零参数锁定方法锁定整个文件,即使它在增长。 底层操作系统必须提供对独占锁或共享锁的支持。如果操作系统不支持共享锁,并且请求共享锁,则使用独占锁。可以使用 **FileLock.isShared()** 查询锁的类型 (共享或独占)。 ### 锁定映射文件的某些部分 文件映射通常用于非常大的文件。您可能需要锁定此类文件的某些部分,以便其他进程可以修改未锁定的部分。例如,数据库必须同时对许多用户可用。这里你可以看到两个线程,每个线程都锁定文件的不同部分: ```java // newio/LockingMappedFiles.java // (c)2017 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Locking portions of a mapped file import java.nio.*; import java.nio.channels.*; import java.io.*; public class LockingMappedFiles { static final int LENGTH = 0x8FFFFFF; // 128 MB static FileChannel fc; public static void main(String[] args) throws Exception { fc = new RandomAccessFile("test.dat", "rw") .getChannel(); MappedByteBuffer out = fc.map( FileChannel.MapMode.READ_WRITE, 0, LENGTH); for(int i = 0; i < LENGTH; i++) out.put((byte)'x'); new LockAndModify(out, 0, 0 + LENGTH/3); new LockAndModify( out, LENGTH/2, LENGTH/2 + LENGTH/4); } private static class LockAndModify extends Thread { private ByteBuffer buff; private int start, end; LockAndModify(ByteBuffer mbb, int start, int end) { this.start = start; this.end = end; mbb.limit(end); mbb.position(start); buff = mbb.slice(); start(); } @Override public void run() { try { // Exclusive lock with no overlap: FileLock fl = fc.lock(start, end, false); System.out.println( "Locked: "+ start +" to "+ end); // Perform modification: while(buff.position() < buff.limit() - 1) buff.put((byte)(buff.get() + 1)); fl.release(); System.out.println( "Released: " + start + " to " + end); } catch(IOException e) { throw new RuntimeException(e); } } } } /* Output: Locked: 75497471 to 113246206 Locked: 0 to 50331647 Released: 75497471 to 113246206 Released: 0 to 50331647 */ ``` **LockAndModify** 线程类设置缓冲区并创建要修改的 **slice()**,在 **run()** 中,锁在文件通道上获取(不能在缓冲区上获取锁—只能在通道上获取锁)。**lock()** 的调用非常类似于获取对象上的线程锁——现在有了一个“临界区”,可以对文件的这部分进行独占访问。[^1] 当 JVM 退出或关闭获取锁的通道时,锁会自动释放,但是您也可以显式地调用 **FileLock** 对象上的**release()**,如上所示。 1. 您可以在附录:低级并发中找到关于线程的更多细节。 =======