Saturday, January 22, 2022

Java Tutorial: FileChannel

Chapters

FileChannel

FileChannel is a SeekableByteChannel that is connected to a file. File channel can reposition its file pointer to any position in a file just like RandomAccessFile; can force file updates to go directly to the storage disk, can map region of files into memory, which is often efficient for large files; can lock a region of a file; and can access by multiple threads.

Even though file channel can be accessed by multiple threads, FileChannel operations are blocking operations. Although, according to the documentation:

"Other operations, in particular those that take an explicit position, may proceed concurrently; whether they in fact do so is dependent upon the underlying implementation and is therefore unspecified."

My explanation here is simplified. More information can be found in the documentation.

Opening a FileChannel

There are two ways of opening a FileChannel. We can use the open() methods in the FileChannel class or use the getChannel() from file-stream-based classes like RandomAccessFile, FileInputStream and others.

This example demonstrates opening a FileChannel using open() and reading file content using FileChannel.
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.io.ByteArrayOutputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    Path source = Paths.get("C:\\test\\fileTest.txt");
    
    if(!Files.exists(source)){
      System.err.println("source doesn't exist!");
      return;
    }
    
    String content = null;
    try(FileChannel fc = 
    FileChannel.open(source, StandardOpenOption.READ);
    ByteArrayOutputStream out =
    new ByteArrayOutputStream()){
      
      int buffer = 
      (1024L > fc.size()) ? 
      (int)fc.size() :
      1024;
      
      ByteBuffer bb = 
      ByteBuffer.allocate(buffer);
      
      while(fc.read(bb) > 0){
        out.write(bb.array());
        bb.clear();
      }
      
      content = 
      new String(out.toByteArray(),
                 StandardCharsets.UTF_8);
      
    }
    
    if(content != null)
      System.out.println("Content: " + content);
    else System.err.println("content is null!");
    
  }
}
In the example above, I use this form of open():
public static FileChannel open(Path path, OpenOption... options)throws IOException
StandardOpenOption.READ grants read access to file channel. Read OpenOption and StandardOpenOption for more options that can be put in this method.

In order to read and write from FileChannel, we need to use ByteBuffer. FileChannel reads and writes data from ByteBuffer. Then, in the example above, I write the read data to ByteArrayOutputStream.

allocate() method sets the buffer size of ByteBuffer. array() method in ByteBuffer wraps bytes in a byte[] array. StandardCharsets.UTF_8 encodes bytes into UTF_8 characters. size() method returns the current size of the file that a FileChannel is connected to.

Another way of opening a FileChannel is to open a file-stream-based class like FileOutputStream and use its getChannel() method. This example demonstrates getChannel() and writing to a FileChannel.
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.io.FileOutputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    Path source = Paths.get("C:\\test\\test.txt");
    
    try(FileOutputStream out =
    new FileOutputStream(source.toFile())){
    
      out.write(
      new String("ABCDEFGHIJK").
          getBytes(StandardCharsets.UTF_8));
          
      try(FileChannel fc = out.getChannel()){
      
        ByteBuffer bb = 
        ByteBuffer.wrap(new String("LMNOPQRSTUVWXYZ").
        getBytes(StandardCharsets.UTF_8));
        
        while(bb.hasRemaining()){
          fc.write(bb);
        }
      }
    }
    System.out.println("Writing Complete!");
  }
}

Result(C:\test\test.txt content)
ABCDEFGHIJKLMNOPQRSTUVWXYZ
FileChannel that is opened using getChannel() follows the file pointer and mode of the source file-stream. That's why the written data in FileChannel didn't overwrite the written data in FileOutputStream because FileChannel followed the file pointer of FileOutputStream.

wrap(byte[] array) method sets the new buffer's capacity and limit to the length of the array in the argument. More information can be found in the documentation. Remember that we can't open a file channel if the source stream is closed.

In the example above, file channels are automatically closed by try-with-resources. If you want to manually close a file channel, use the close() method.

force() Method

Method form: public abstract void force(boolean metaData) throws IOException
Sometimes, Operating systems may cache file changes instead of sending file changes directly to the file in the storage device. This method Forces any updates to this channel's file to be written to the storage device that contains it.

If this channel's file resides on a local storage device then when this method returns it is guaranteed that all changes made to the file since this channel was created, or since this method was last invoked, will have been written to that device.

This is useful for ensuring that critical information is not lost in the event of a system crash. If the file does not reside on a local device then no such guarantee is made. To enable the feature discussed above, just enable it by setting force() argument to true. More information can be found in the documentation
try(FileChannel fc = out.getChannel()){
  
  //enable force update
  fc.force(true);
  
  ByteBuffer bb = 
  ByteBuffer.wrap(new String("LMNOPQRSTUVWXYZ").
  getBytes(StandardCharsets.UTF_8));
        
  while(bb.hasRemaining()){
    fc.write(bb);
  }
}
map() Method

Method Form: public abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) throws IOException
Maps a region of this channel's file directly into memory. This method accepts three type of modes:
READ_ONLY
READ_WRITE
PRIVATE
More information can be found in the documentation. More information about modes can be found in this documentation.

This example demonstrates map().
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    Path source = Paths.get("C:\\test\\test.txt");
    try(FileChannel fc = 
    FileChannel.open(
    source, 
    StandardOpenOption.WRITE,
    StandardOpenOption.READ)){
    
      MappedByteBuffer mBuff =
      fc.map(FileChannel.MapMode.PRIVATE, 5, 5);
      
      ByteBuffer bb = 
      ByteBuffer.wrap(new String("ZZZZZ").
      getBytes(StandardCharsets.UTF_8));
      
      //Note: if we put a ByteBuffer that has
      //size greater than the capacity of
      //mBuff, this method will throw
      //BufferOverflowException
      mBuff.put(bb);
    }
    System.out.println("Writing Complete!");
  }
}
Assume that C:\test\test.txt exists in your machine and has this content:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
MapMode.PRIVATE ensures that any changes to mapped data of a file in memory won't affect the file. Thus, the example above won't do any changes to test.txt

Now, in the example above, try changing the map mode to MapMode.READ_WRITE and run the example again. This time the content of test.txt is going to be like this:
ABCDEZZZZZKLMNOPQRSTUVWXYZ

If you want the mapped data to be read-only, use MapMode.READ. map() method returns MappedByteBuffer which is a subclass of ByteBuffer.

position() Method

This method has two forms. position() returns current position of FileChannel's file pointer. position(long newPosition) sets FileChannel's new position.

This example demonstrates both forms.
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
  
    Path source = Paths.get("C:\\test\\test.txt");
    
    if(!Files.exists(source)){
      System.err.println("source doesn't exist!");
      return;
    }
    
    try(FileChannel fc = 
    FileChannel.open(source, StandardOpenOption.READ)){
      
      ByteBuffer bb = 
      ByteBuffer.allocate(1);
      
      System.out.println
      ("pos: " + fc.position());
      fc.read(bb);
      System.out.println("content: " +
      new String(bb.array(),
                 StandardCharsets.UTF_8));
      bb.clear();
      
      System.out.println("set pos...");
      fc.position(7);
      
       System.out.println
      ("pos: " + fc.position());
      fc.read(bb);
      System.out.println("content: " +
      new String(bb.array(),
                 StandardCharsets.UTF_8));
      bb.clear();
    }
    
  }
}
Assume that C:\test\test.txt exists in your machine and has this content:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
The result is going to be:
pos: 0
content: A
set pos...
pos: 7
content: H
truncate() Method

Method Form: public abstract FileChannel truncate(long size)throws IOException
Truncates this channel's file to the given size. If the given size is less than the file's current size then the file is truncated, discarding any bytes beyond the new end of the file.

If the given size is greater than or equal to the file's current size then the file is not modified. In either case, if this channel's file position is greater than the given size then it is set to that size.

This example demonstrates truncate().
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    Path source = Paths.get("C:\\test\\test.txt");
    
    if(!Files.exists(source)){
      System.err.println("source doesn't exist!");
      return;
    }
    
    try(FileChannel fc = 
    FileChannel.open(source, StandardOpenOption.WRITE)){
      //value in the argument is in byte
      //not bit
      fc.truncate(16L);
    }
    System.out.println("file truncated!");
  }
}
Assume that C:\test\test.txt exists in your machine and has this content:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Then, test.txt has a file size of 26 bytes. In the example above, the file size will be reduced to 16 bytes.

transferFrom() Method

Method Form: public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException
Transfers bytes into this channel's file from the given readable byte channel. More information can be read in the documentation. This method can be used for transferring FileChannel data to another FileChannel. We can also use this method to transfer data from channels that implements ReadableByteChannel to this FileChannel.

This example demonstrates transferFrom().
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    Path source1 = Paths.get("C:\\test\\test1.txt");
    Path source2 = Paths.get("C:\\test\\test2.txt");
    
    try(FileChannel fc1 = 
    FileChannel.open(source1,StandardOpenOption.READ);
    FileChannel fc2 = 
    FileChannel.open(source2, StandardOpenOption.WRITE)){
      fc2.transferFrom(fc1, 0, fc1.size());
    }
    System.out.println("Operation Complete!");
  }
}
Note that freshly written data into the source(fc1) before calling this method won't be transferred. This method is potentially much more efficient than a simple loop that reads from the source channel and writes to this channel. Many operating systems can transfer bytes directly from the source channel into the filesystem cache without actually copying them.

transferTo() Method

Method Form: public abstract long transferTo(long position, long count, WritableByteChannel target)throws IOException
Transfers bytes from this channel's file to the given writable byte channel. More information can be found in the documentation. This method is the inverted version of transferFrom().

This method demonstrates transferTo().
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    Path source1 = Paths.get("C:\\test\\test1.txt");
    Path source2 = Paths.get("C:\\test\\test2.txt");
    
    try(FileChannel fc1 = 
    FileChannel.open(source1,StandardOpenOption.READ);
    FileChannel fc2 = 
    FileChannel.open(source2, StandardOpenOption.WRITE)){
      fc1.transferTo(0, fc1.size(), fc2);
    }
    System.out.println("Operation Complete!");
  }
}
Note that freshly written data into the source(fc1) before calling this method won't be transferred. This method is potentially much more efficient than a simple loop that reads from the source channel and writes to this channel. Many operating systems can transfer bytes directly from the source channel into the filesystem cache without actually copying them.

Locking FileChannel

We can use two methods to lock file access of file channels. lock() and tryLock() are methods that returns FileLock. lock() and tryLock() have second forms:
tryLock(long position, long size, boolean shared)
lock(long position, long size, boolean shared)
These forms lock a region of a file. Their first forms lock the entire region of file. the shared is a boolean flag than enables/disables shared lock. FileLock has two types of lock: Exclusive and Shared lock.

Shared lock enables thread to acquire overlapping shared locks. FileLock locks may overlap one other if the regions they're locking overlap. For example, lock1 locks position 5-10 then lock2 locks position 8-13. lock1 and lock2 are overlapping. If FileLock has shared lock, FileChannel that has lock2 can acquire lock1.

Note that shared lock can't acquire overlapping exclusive lock. Exclusive lock doesn't allow acquiring of either overlapping lock types. Also, remember that File locks are held on behalf of the entire Java virtual machine. They are not suitable for controlling access to a file by multiple threads within the same virtual machine. More information can be found in the documentation.

If you want locks that are suitable for a single virtual machine, you may consider using locks in java.util.concurrent.locks. The difference between lock() and tryLock() are explained there. This article that I created explains locks in java.util.concurrent.locks.

This snippet demonstrates lock(long position, long size, boolean shared).
try(FileChannel fc = 
    FileChannel.open(path, StandardOpenOption.READ)){
      FileLock lock = fc.lock(5,10,false);
      System.out.println(Thread.currentThread().getName() + 
      " acquired the lock.");
      //other tasks...
      lock.release();
    }

No comments:

Post a Comment