Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

File Handling and I/O Operations in Java

Tech May 10 4

Understanding Files

In a narrow sense, a file refers to files and directories (often called folders) on a hard disk.

In a broader sense, a file represents hardware resources in a computer. Operating systems abstract many hardware devices and software resources as files and manage them in a file-like manner.

File Paths

Every file on a hard disk has a specific "path" that describes its location.

Example of an absolute file path

In early operating systems, directories were separated using /. Later, when Microsoft released DOS, they temporarily changed it to \. Due to public backlash, they eventually supported both. Today, \ is the default, but using / works without any issues.

There are two styles for representing paths:

  1. Absolute path: Starts with a drive letter, such as C: or D:. Absolute path example

  2. Relative path: Based on the current working directory. Terminal showing relative path

Directory structure example

Assume the goal is to locate the directory 111.

  • Absolute path: d:/tmp/111
  • Relative path if the working directory is d:/: ./tmp/111
  • Relative path if the working directory is d:/tmp: ./111
  • Relative path if the working directory is d:/tmp/222: ../111 (.. refers to the parent directory)
  • Relative path if the working directory is d:/tmp/222/bbb: ../../111

Working with Files in Java

Java provides two main areas of file manipulation:

  1. File system operations (creating, deleting, renaming files, etc.)
  2. File content operations (reading and writing)

The standard library includes the File class for these purposes.

File Class

Attributes

Modifier and Type Attribute Description
static String pathSeparator System-dependent path separator, represented as a String (/ or \)
static char pathSeparatorChar System-dependent path separator, represented as a char

Constructors

Signature Description
File(File parent, String child) Creates a new File instance from a parenet directory and a childd file path.
File(String pathname) Creates a new File instance from a path string, which can be absolute or relative.
File(String parent, String child) Creates a new File instance from a parent path string and a child file path.

Methods

Modifier and Return Type Method Signature Description
String getName() Returns the pure file name.
String getParent() Returns the parent directory path.
String getPath() Returns the file path.
String getAbsolutePath() Returns the absolute path.
String getCanonicalPath() Returns the canonical (simplified) absolute path.
boolean exists() Checks if the abstract pathname actually exists on the file system.
boolean isDirectory() Checks if the pathname represents a directory.
boolean isFile() Checks if the pathname represents a regular file.
boolean createNewFile() Atomically creates a new, empty file. Returns true if the file was created.
boolean delete() Deletes the file or directory. Returns true on success.
void deleteOnExit() Marks the file for deletion when the JVM terminates.
String[] list() Returns an array of names of files and directories in the directory.
File[] listFiles() Returns an array of File objects representing files and directories.
boolean mkdir() Creates the directory.
boolean mkdirs() Creates the directory, including any necessary parent directories.
boolean renameTo(File dest) Renames/moves the file.
boolean canRead() Tests if the application can read the file.
boolean canWrite() Tests if the application can modify the file.

Examples

Example 1: Observing path-related methods

import java.io.File;
import java.io.IOException;

public class PathInspector {
    public static void main(String[] args) throws IOException {
        File entry = new File("./sample.txt"); // The file does not need to actually exist
        System.out.println("Name: " + entry.getName());
        System.out.println("Parent: " + entry.getParent());
        System.out.println("Path: " + entry.getPath());
        System.out.println("Absolute: " + entry.getAbsolutePath());
        System.out.println("Canonical: " + entry.getCanonicalPath());
    }
}

Example 2: Creating a regular file

import java.io.File;
import java.io.IOException;

public class FileGenerator {
    public static void main(String[] args) throws IOException {
        File document = new File("./sample.txt");
        document.createNewFile();
        System.out.println("Exists: " + document.exists());
        System.out.println("Is File: " + document.isFile());
        System.out.println("Is Directory: " + document.isDirectory());
    }
}

A new file named hello.txt is created after execution

Example 3: Deleting a regular file

import java.io.File;

public class FileEraser {
    public static void main(String[] args) {
        File document = new File("./sample.txt");
        document.delete();
    }
}

The file hello.txt is deleted after execution

Example 4: Using deleteOnExit for temporary files

The deleteOnExit method is used for temporary files; the file is automatically deleted when the program exits.

import java.io.File;
import java.io.IOException;

public class TempFileHandler {
    public static void main(String[] args) throws IOException {
        File tempData = new File("cache-data.txt");
        System.out.println("Before create: " + tempData.exists());
        System.out.println("Creation success: " + tempData.createNewFile());
        System.out.println("After create: " + tempData.exists());
        tempData.deleteOnExit();
        System.out.println("After marking for deletion: " + tempData.exists());
        // The file still exists until the JVM shuts down
    }
}

Console output demonstrating that the file is created and still exists after deleteOnExit is called

The file is indeed created, and deleteOnExit will remove it, but only after the program terminates.

Example 5: Creating directories

import java.io.File;

public class DirectoryBuilder {
    public static void main(String[] args) {
        File singleFolder = new File("./newDir");
        File nestedFolders = new File("./project/module/src/");

        singleFolder.mkdir();   // Creates a single directory
        nestedFolders.mkdirs(); // Creates all necessary parent directories
    }
}

The directories 'hello' and 'test/aaa/bbb' are created

Example 6: Renaming files

import java.io.File;

public class FileRenamer {
    public static void main(String[] args) {
        File originalDir = new File("./oldName");
        File newDir = new File("./newName");
        originalDir.renameTo(newDir);
    }
}

The directory 'test' is renamed to 'testAAA'

Reading and Writing File Content – Stream Objects

A "stream" is an illustrative metaphor. Imagine needing 100 ml of water; you could take it all at once, 50 ml at a time, or 10 ml at a time. Reading and writing files works similarly. Stream objects allow flexible, granular operations.

Java's standard library provides two main categories of streams:

  1. Byte Streams (for binary data)

    • InputStream (abstract class) – FileInputStream
    • OutputStream (abstract class) – FileOutputStream
  2. Character Streams (for text data)

    • Reader (abstract class) – FileReader
    • Writer (abstract class) – FileWriter

The core usage pattern for these classes involves four operations:

  1. Open the file (via constructor)
  2. Read from the file (read method) – for InputStream / Reader
  3. Write to the file (write method) – for OutputStream / Writer
  4. Close the file (close method)

Why close? Each time a file is opened, the process's kernel data structure (PCB) allocates a file descriptor table entry. This table has a limit. If files are opened and not closed, the table can fill up, preventing further file access.

Diagram illustrating the relationship between a process, its PCB, a file descriptor table, and a file on the hard disk

Byte Streams

InputStream (Reading bytes from hard disk into memory)

Diagram of InputStream and its implementations

Reading a text file:

Contents of test.txt showing 'hello'

Three overloaded read methods:

  1. read(): Reads a single byte.
  2. read(byte[] b): Reads bytes into a byte array (the parameter acts as an output parameter).
  3. read(byte[] b, int off, int len): Reads up to len bytes into a portion of the array.

Example with read() (no arguments):

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class ByteFileReader {
    public static void main(String[] args) throws IOException {
        // 'data.txt' on D: drive contains the string "hello"
        InputStream dataStream = new FileInputStream("d:/data.txt");

        while (true) {
            int singleByte = dataStream.read();
            if (singleByte == -1) {
                break; // End of file reached
            }
            System.out.println(singleByte);
        }
        dataStream.close();
    }
}

The return value of read is an int, not byte. While a byte value ranges from 0 to 255, the special value -1 is used to signal the end of the file.

ASCII codes of the characters 'h','e','l','l','o' are printed

Example with read(byte[] b) (array version):

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class BufferedFileReader {
    public static void main(String[] args) throws IOException {
        InputStream dataStream = new FileInputStream("d:/data.txt");

        while (true) {
            byte[] buffer = new byte[1024];
            int bytesRead = dataStream.read(buffer);
            System.out.println("Bytes read this cycle: " + bytesRead);
            if (bytesRead == -1) {
                break;
            }
            // The read data is now in the buffer.
            // Further processing, e.g., printing in hex:
            // for (int i = 0; i < bytesRead; i++) {
            //     System.out.printf("%x\n", buffer[i]);
            // }
        }
        dataStream.close();
    }
}

I/O operations, which access the hard disk, are relatively time-consuming. Frequent I/O adds overhead. Reading a large chunk (e.g., 1024 bytes) at once reduces the number of read operations, improving overall program efficiency compared to reading one byte at a time.

Using byte streams for a text file is functional but not as convenient as using character streams.

OutputStream (Writing bytes from memory to hard disk)

OutputStream is an abstract class. The FileOutputStream implementation is used for writing to files.

Original content of test.txt is '你好' (Chinese for 'hello')

import java.io.*;

public class ByteFileWriter {
    public static void main(String[] args) throws IOException {
        try (OutputStream outputStream = new FileOutputStream("d:/data.txt")) {
            outputStream.write(97);  // 'a'
            outputStream.write(98);  // 'b'
            outputStream.write(99);  // 'c'
            outputStream.write(100); // 'd'
        } // The stream is automatically closed here
    }
}

After execution, the content of test.txt is 'abcd', overwriting the previous content

This code snippet uses the try-with-resources statement. When the try block finishes, the close method is automatically called because FileOutputStream implements the Closeable interface.

Character Streams

Reader

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class TextFileReader {
    public static void main(String[] args) {
        try (Reader textReader = new FileReader("d:/data.txt")) {
            while (true) {
                int ch = textReader.read();
                if (ch == -1) {
                    break;
                }
                System.out.print((char) ch);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Writer

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class TextFileWriter {
    public static void main(String[] args) {
        try (Writer textWriter = new FileWriter("d:/data.txt")) {
            textWriter.write("hello world");
            textWriter.flush(); // Manually flush the buffer to disk
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

After a write operation, data might reside in an internal buffer and not yet on the physical disk. Calling flush() forces the buffer to be written. Alternatively, the close method also triggers a flush.

Using Scanner with Files

The Scanner class can also read from files. System.in is itself an input stream.

Scanner class constructors accepting InputStream

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class FileScanner {
    public static void main(String[] args) {
        try (InputStream fileStream = new FileInputStream("d:/data.txt");
             Scanner fileScanner = new Scanner(fileStream)) {

            while (fileScanner.hasNext()) {
                String word = fileScanner.next();
                System.out.println(word);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Comprehensive Examples

Search and Conditional Delete

This program scans a specified directory, finds all regular files (not directories) whose names contain a user-specified string, and prompts the user about deleting each one.

import java.io.File;
import java.util.Scanner;

public class SelectiveFileDeleter {
    private static Scanner inputCollector = new Scanner(System.in);

    public static void main(String[] args) {
        System.out.println("Enter the directory path to search: ");
        String basePath = inputCollector.next();

        File rootDir = new File(basePath);
        if (!rootDir.isDirectory()) {
            System.out.println("Error: The provided path is not a valid directory.");
            return;
        }

        System.out.println("Enter the filename substring to search for deletion: ");
        String pattern = inputCollector.next();

        traverseAndDelete(rootDir, pattern);
    }

    private static void traverseAndDelete(File rootDir, String pattern) {
        System.out.println("[Scanning] " + rootDir.getAbsolutePath());
        File[] contents = rootDir.listFiles();
        if (contents == null) {
            return; // Directory is empty or inaccessible
        }

        for (File item : contents) {
            if (item.isDirectory()) {
                traverseAndDelete(item, pattern);
            } else {
                if (item.getName().contains(pattern)) {
                    System.out.println("Delete " + item.getAbsolutePath() + "? (y/n)");
                    String confirmation = inputCollector.next();
                    if (confirmation.equalsIgnoreCase("y")) {
                        boolean deleted = item.delete();
                        System.out.println("Deleted: " + deleted);
                    } else {
                        System.out.println("Skipped.");
                    }
                }
            }
        }
    }
}

File Copy

This program copies a file from a source path to a destination path.

import java.io.*;
import java.util.Scanner;

public class FileDuplicator {
    public static void main(String[] args) {
        Scanner pathScanner = new Scanner(System.in);
        System.out.println("Enter the source file path: ");
        String srcPath = pathScanner.next();
        System.out.println("Enter the destination file path: ");
        String destPath = pathScanner.next();

        File source = new File(srcPath);
        if (!source.isFile()) {
            System.out.println("Error: The source must be an existing file.");
            return;
        }

        File destination = new File(destPath);
        if (destination.isFile()) {
            System.out.println("Error: The destination file already exists.");
            return;
        }

        // Perform the copy using try-with-resources for both input and output streams
        try (InputStream inStream = new FileInputStream(source);
             OutputStream outStream = new FileOutputStream(destination)) {

            while (true) {
                int data = inStream.read();
                if (data == -1) {
                    break;
                }
                outStream.write(data);
            }
            System.out.println("File copy complete.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Note: The try-with-resources statement can manage multiple resources separated by semicolons.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.