javafilepathnio

How is Java Path converted to e.g. InputStream


The java.nio.Files class has a static method named Files#newInputStream, which takes a Path instance as an input and returns an InputStream as an output. But it is unclear to me how this can be done without instantiating a File(InputStream) object. I would like to be able to make my own implementation of Path which can point to an InputStream which is not related to a File object. I would like to do this so that I can make a virtual filesystem which uses the notation of filesystems without actually depending on a filesystem. Is this possible?


Solution

  • Java NIO Design

    The Java NIO file system API uses delegation and abstract factory patterns. At the lowest level there's the implementation of java.nio.file.spi.FileSystemProvider:

    Service-provider class for file systems. The methods defined by the Files class will typically delegate to an instance of this class. [emphasis added]

    A FileSystemProvider provides instances of java.nio.file.FileSystem:

    Provides an interface to a file system and is the factory for objects to access files and other objects in the file system.

    The default file system, obtained by invoking the FileSystems.getDefault method, provides access to the file system that is accessible to the Java virtual machine. The FileSystems class defines methods to create file systems that provide access to other types of (custom) file systems.

    A file system is the factory for several types of objects: [emphasis added]

    • The getPath method converts a system dependent path string, returning a Path object that may be used to locate and access a file.

    • The getPathMatcher method is used to create a PathMatcher that performs match operations on paths.

    • The getFileStores method returns an iterator over the underlying file-stores.

    • The getUserPrincipalLookupService method returns the UserPrincipalLookupService to lookup users or groups by name.

    • The newWatchService method creates a WatchService that may be used to watch objects for changes and events.

    And then there's the Path interface:

    An object that may be used to locate a file in a file system. It will typically represent a system dependent file path.

    Note that a Path is just that, a path. It does not necessarily represent an actual file, only the path of the file (whether it exists or not). It's similar to java.io.File in this regard.

    When you invoke methods in the Files class it uses the Path#getFileSystem() and FileSystem#provider() methods to delegate to the FileSystemProvider. This API design is what allows a developer to use the Files class with any implementation of Path in a transparent manner. It doesn't matter to the developer how that InputStream is created, only that the InputStream is created in a way consistent with the API's contracts.

    The methods Paths#get(String,String...) and Path#of(String,String...) delegate to the default file system to create the instance of Path. When you use those Path instances with the methods in Files you end up accessing the host platform's underlying file system.

    Creating InputStream

    To see the aforementioned delegation in action, check out the source code of Files#newInputStream(Path,OpenOption...) (link and code for Java 24):

    public static InputStream newInputStream(Path path, OpenOption... options)
        throws IOException
    {
        return provider(path).newInputStream(path, options);
    }
    
    // [...]
    
    /**
     * Returns the {@code FileSystemProvider} to delegate to.
     */
    private static FileSystemProvider provider(Path path) {
        return path.getFileSystem().provider();
    }
    

    It simply delegates to FileSystemProvider#newInputStream(Path,OpenOption...). It's up to the implementation of that method to determine how to open the input stream for the given path and options. For instance, the default file systems will return an input stream that reads from the native file system. Whereas an in-memory file system might return a ByteArrayInputStream or something similar.

    Virtual (in-memory) File System

    If you want to create a virtual file system then you need to implement a FileSystemProvider along with all the related abstract classes and interfaces, such as FileSystem and Path. Ultimately, these implementations will likely store the files in one or more byte[] or ByteBuffer. The streams and channels will read from/write to these arrays/buffers.

    Note some of the API is "optional". If your implementation doesn't provide the optional API then you can throw an UnsupportedOperationException. The Javadoc of the various classes/methods gives more information.

    That said, there's already an in-memory file system implementation out there: https://github.com/google/jimfs

    Relationship to java.io.File

    "But it is unclear to me how this can be done without instantiating a File(InputStream)". The NIO file API is not related to java.io.File.

    If you want to see how it does what it does you can look at the source code. Your JDK should have come with a src.zip file which contains the Java source files; however, it will only contain the implementation for your host operating system and won't contain any of the native code (the default file systems ultimately use native code to communicate with the underlying OS' file system).

    Browse the GitHub repository to see all the source code, native included, for all operating systems.