推荐关注公众号:锅外的大佬 每日推送国外技术好文,帮助每位开发者更优秀地成长 原文链接:https://www.baeldung.com/java-filechannel 作者:baeldung 译者:Leesen 1.概述 在这篇速学教程中,我们将研究Java NIO库中提供的FileChannel类,讨论如何使用FileChannel和ByteBuffer读写数据,探讨使用FileChannel以及其他文件操作特性的优点。 2.FileChannel的优点 FileChannel的优点包括: 在文件特定位置进行读写操作 将文件一部分直接加载到内存,这样效率更高 以更快的速度将文件数据从一个通道传输到另一个通道 锁定文件的某一部分来限制其他线程访问 为了避免数据丢失,强制立即将更新写入文件并存储 3.FileChannel读操作 当我们读取一个大文件时,FileChannel比标准I/O执行得更快。需要注意,虽然FileChannel是Java NIO的一部分,但是FileChannel操作是阻塞的,并且没有非阻塞模式。 3.1.使用FileChannel读取文件 先了解如何使用FileChannel读取一个文件,该文件包含: Hello world 下面测试读取文件,并检查是否ok: @Test public void givenFile_whenReadWithFileChannelUsingRandomAccessFile_thenCorrect() throws IOException { try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r"); FileChannel channel = reader.getChannel(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { int bufferSize = 1024; if (bufferSize > channel.size()) { bufferSize = (int) channel.size(); } ByteBuffer buff = ByteBuffer.allocate(bufferSize); while (channel.read(buff) > 0) { out.write(buff.array(), 0, buff.position()); buff.clear(); } String fileContent = new String(out.toByteArray(), StandardCharsets.UTF_8); assertEquals("Hello world", fileContent); } } 这里使用FileChannel、RandomAccessFile和ByteBuffer从文件中读取字节。还应该注意,多个并发线程可以安全地使用FileChannel。但是,每次只允许一个线程执行涉及更新通道位置(channel position)或更改其文件大小的操作。这会阻止其他试图执行类似操作的线程,直到前一个操作完成。 但是,显式提供通道位置的操作可以并发运行且不会被阻塞。 3.2.打开FileChannel 为了使用FileChannel读取文件,我们必须打开它(Open FileChannel)。看看如何使用RandomAccessFile打开FileChannel: RandomAccessFile reader = new RandomAccessFile(file, "r"); FileChannel channel = reader.getChannel(); 模式“r”表示通道仅为“只读“,注意,关闭RandomAccessFile也将关闭与之关联的通道。 接下来,使用FileInputStream打开一个FileChannel来读取文件: FileInputStream fin= new FileInputStream(file); FileChannel channel = fin.getChannel(); 同样的,关闭FileInputStream也会关闭与之相关的通道。 3.3.从FileChannel中读取数据 为了读取数据,我们可以使用只读模式。接下来看看如何读取字节序列,我们将使用ByteBuffer来保存数据: ByteBuffer buff = ByteBuffer.allocate(1024); int noOfBytesRead = channel.read(buff); String fileContent = new String(buff.array(), StandardCharsets.UTF_8); assertEquals("Hello world", fileContent); 然后,我们将看到如何从文件某个位置开始读取一个字节序列: ByteBuffer buff = ByteBuffer.allocate(1024); int noOfBytesRead = channel.read(buff, 5); String fileContent = new String(buff.array(), StandardCharsets.UTF_8); assertEquals("world", fileContent); 我们应该注意:需要使用字符集(Charset)将字节数组解码为字符串 我们指定原始编码字节的字符集。没有它,我们可能会以断章取义的文字结束。特别是像UTF-8和UTF-16这样的多字节编码可能无法解码文件的任意部分,因为一些多字节字符可能是不完整的。 4.FileChannel写操作 4.1.使用FileChannel写入文件 我们来探究下如何使用FileChannel写: @Test public void whenWriteWithFileChannelUsingRandomAccessFile_thenCorrect() throws IOException { String file = "src/test/resources/test_write_using_filechannel.txt"; try (RandomAccessFile writer = new RandomAccessFile(file, "rw"); FileChannel channel = writer.getChannel()){ ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8)); channel.write(buff); // verify RandomAccessFile reader = new RandomAccessFile(file, "r"); assertEquals("Hello world", reader.readLine()); reader.close(); } } 4.2.打开FileChannel 要使用FileChannel写入文件,必须先打开它。使用RandomAccessFile打开一个FileChannel: RandomAccessFile writer = new RandomAccessFile(file, "rw"); FileChannel channel = writer.getChannel(); 模式“rw”表示通道为“读写”。 使用FileOutputStream打开FileChannel: FileOutputStream fout = new FileOutputStream(file); FileChannel channel = fout.getChannel(); 4.3.FileChannel写入数据 使用FileChannel写数据,可以使用其中的某个写方法。 我们来看下如何写一个字节序列,使用ByteBuffer来存储数据: ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8)); channel.write(buff); 接下来,我们将看到如何从文件某个位置开始写一个字节序列: ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8)); channel.write(buff, 5); 5.当前位置 FileChannel允许我们获得和改变读或写的位置(position)。获得当前的位置: long originalPosition = channel.position(); 设置位置: channel.position(5); assertEquals(originalPosition + 5, channel.position()); 6.获取文件大小 使用FileChannel.size方法获取文件大小(以字节为单位): @Test public void whenGetFileSize_thenCorrect() throws IOException { RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r"); FileChannel channel = reader.getChannel(); // the original file size is 11 bytes. assertEquals(11, channel.size()); channel.close(); reader.close(); } 7.截断文件 使用FileChannel.truncate方法将文件截断为给定的大小(以字节为单位): @Test public void whenTruncateFile_thenCorrect() throws IOException { String input = "this is a test input"; FileOutputStream fout = new FileOutputStream("src/test/resources/test_truncate.txt"); FileChannel channel = fout.getChannel(); ByteBuffer buff = ByteBuffer.wrap(input.getBytes()); channel.write(buff); buff.flip(); channel = channel.truncate(5); assertEquals(5, channel.size()); fout.close(); channel.close(); } 8.强制更新 由于性能原因,操作系统可能缓存文件更改,如果系统崩溃,数据可能会丢失。要强制文件内容和元数据不断写入磁盘,我们可以使用force方法: channel.force(true); 仅当文件存储在本地设备上时,才能保证该方法有效。 9.将文件部分加载到内存 使用FileChannel.map方法将文件的部分加载到内存中。使用FileChannel.MapMode.READ_ONLY以只读模式打开文件: @Test public void givenFile_whenReadAFileSectionIntoMemoryWithFileChannel_thenCorrect() throws IOException { try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r"); FileChannel channel = reader.getChannel(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 6, 5); if(buff.hasRemaining()) { byte[] data = new byte[buff.remaining()]; buff.get(data); assertEquals("world", new String(data, StandardCharsets.UTF_8)); } } } 类似地,可以使用FileChannel.MapMode.READ_WRITE以读写模式打开文件。还可以使用FileChannel.MapMode.PRIVATE模式,该模式下,更改不应用于原始文件。 10.锁定文件部分 来看下如何锁定文件某一部分,使用FileChannel.tryLock方法阻止对文件某一部分进行高并发访问。 @Test public void givenFile_whenWriteAFileUsingLockAFileSectionWithFileChannel_thenCorrect() throws IOException { try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "rw"); FileChannel channel = reader.getChannel(); FileLock fileLock = channel.tryLock(6, 5, Boolean.FALSE )){ //do other operations... assertNotNull(fileLock); } } tryLock方法尝试获取文件部分(file section)上的锁。如果请求的文件部分已被另一个线程阻塞,它将抛出一个OverlappingFileLockException异常。此方法还接受Boolean参数来请求共享锁或独占锁。 我们应该注意到,有些操作系统可能不允许共享锁,默认情况下是独占锁。 11.FileChannel关闭 最后,当使用FileChannel时,必须关闭它。在示例中,我们使用了try-with-resources。 如果有必要,我们可以直接使用FileChannel.close方法: channel.close(); 12.总结 在本教程中,我们了解了如何使用FileChannel读取和写入文件。此外,我们还研究了如何读取和更改文件大小及其当前读/写位置,并研究了如何在并发应用程序或数据关键应用程序中使用FileChannel。https://www.cnblogs.com/liululee/p/10921299.html