Java I/O API

java.io package

java.io is a package in the Java Standard Library that provides classes and interfaces for system input and output through data streams, serialization, and the file system. It includes a wide range of classes that facilitate reading from and writing to different data sources such as files, network connections, and memory buffers.

Why Java I/O

  • How to work with files and directories?
  • How to read and write data in binary and text format?
  • How to read from and write to data sources like files, console, or network?
  • What are the performance implications of reading large files byte-by-byte, all at once, and in chunks?
  • How to serialize and deserialize data?

Creating Files 📝 and Directories 📁

To create new files or directories (also known as folders), we can use the java.io.File class.

Create a File Example 📝

To create a new file in Java, you can use the createNewFile() method of the java.io.File class. This method returns true if the file was created successfully, and false if the file already exists.

The example below creates an empty file in the current working directory, which is the directory from which the application is executed.

OnlineGDB ⚡️ - Try it out now GitLab 💻 - View Code

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.File;
import java.io.IOException;

public class App{

    public void createFile(String filename){
        File file = new File(filename);
        try {
            if (file.createNewFile()) {
                System.out.println("File created: " + file.getAbsolutePath());
            } else {
                System.out.println("File already exists: " + file.getAbsolutePath());
            }
        } catch (IOException e) {
            System.err.println("Error creating file: " + e.getMessage());
        }
    }

    public static void main(String[]args){
        App app = new App();
        app.createFile("example.txt");
    }
}

Create a Directory Example 📁

To create a new directory in Java, you can use the mkdir() method of the java.io.File class. This method returns true if the directory was created successfully, and false if the directory already exists.

The example below creates a new directory in the current working directory, which is the directory from which the application is executed.

OnlineGDB ⚡️ - Try it out now GitLab 💻 - View Code

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import java.io.File;

public class App{

    public void createDirectory(String dirname){
        File dir = new File(dirname);
        if (dir.mkdir()) {
            System.out.println("Directory created: " + dir.getAbsolutePath());
        } else {
            System.out.println("Directory already exists: " + dir.getAbsolutePath());
        }
    }

    public static void main(String[]args){
        App app = new App();
        app.createDirectory("exampleDir");
    }
}

Listing Directories Example

To list the content of a directory in Java, you can use the list() method of the java.io.File class. This method returns an array of files and subdirectories.

The example below lists the content of a directory.

OnlineGDB ⚡️ - Try it out now GitLab 💻 - View Code

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.File;

public class App {

    public void listDirectory(String dirname) {
        File dir = new File(dirname);
        if (dir.exists() && dir.isDirectory()) {
            String[] files = dir.list();
            if (files != null) {
                for (String file : files) {
                    System.out.println(file);
                }
            } else {
                System.out.println("The directory is empty.");
            }
        } else {
            System.out.println("The specified path is not a directory or does not exist.");
        }
    }

    public static void main(String[] args) {
        App app = new App();
        app.listDirectory(".");
    }
}

Stream

A stream is a sequence of data that flows in and out of a program. Streams are used to read data from sources like files or the network, and to write data to destinations like files or the screen.

In Java, a stream represents a flow of data with a writer at one end and a reader at the other.

  • In the Java API, a source from which one can read bytes is called an input stream.
    • The bytes can come from a file, a network connection, or an array in memory.
  • In the Java API, a destination to which one can write bytes is called an output stream.
    • The bytes can be written to a file, a network connection, or an array in memory.

Type of Streams

java types of streams

Stream TypeDescriptionCharacteristicsExamples
Byte StreamsDeals with raw binary data (e.g., images, audio, video) or text in non-default encodings- Operates on 8-bit bytes
- Works with raw bytes, so you need to handle encoding yourself
- Used for binary files such as images, audio, video, or binary data and text in non-default encodings
- Can read text files and handle any character encoding
InputStream, OutputStream
Character StreamsDeals with text data (characters, strings)- Operates on 16-bit characters (Unicode)
- Handles character encoding using the platform’s default character encoding (e.g., UTF-8)
- Used for text files and strings
Reader, Writer
Standard StreamsDeals with standard input, output, and error- Operates on 8-bit bytes
- Does not handle character encoding automatically
- There are three standard streams:
1. System.in for reading input (e.g., keyboard)
2. System.out for writing output (e.g., screen)
3. System.err for writing error messages (e.g., screen)
System.in, System.out, System.err

1) Byte Streams

java byte streams

1.1) Byte InputStream Class Hierarchy

java byte input stream class hierarchy

1.2) Byte OutputStream Class Hierarchy

java byte output stream class hierarchy

2) Character Streams

java character streams

2.1) Reader Class Hierarchy

java character stream reader class hierarchy

2.2) Writer Class Hierarchy

java character stream writer class hierarchy

File Handling with Streams

Reading from a file

To read from a file use either FileInputStream or FileReader depending on what type of data you want to read. If you’re reading raw bytes, use FileInputStream. If you want to read stream of characters, use FileReader.

File Reading with Streams

Reading Raw Bytes (FileInputStream) Example

We will read an audio file using FileInputStream. The audio file can be downloaded here and has a size of 83KB (84960 bytes).

GitLab 💻 - View Code

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.FileInputStream;
import java.io.IOException;

public class App {
    public void readAudioFile(String filename) {
        try (FileInputStream fis = new FileInputStream(filename)) {
            int byteCount = 0;
            int byteData;
            while ((byteData = fis.read()) != -1) {
                byteCount++;
            }
            System.out.println("Total bytes read: " + byteCount);
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        App app = new App();
        app.readAudioFile("birds-sound.mp3");
    }
}
Total bytes read: 84960

Reading Characters (FileReader) Example

We will read a text file using FileReader without buffering. The text file content is listed below contains UTF-8 encoded text including special characters and emojis.

OnlineGDB ⚡️ - Try it out now GitLab 💻 - View Code

sample.txt

1
2
3
4
5
6
Hello, 世界! 
This is a UTF-8 encoded text file.
It contains special characters like:
• Arabic: مرحبا
• Japanese: こんにちは
• Emojis: 👋 🌍 ⭐️

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.FileReader;
import java.io.IOException;

public class App {
    public void readTextFile(String filename) {
        try (FileReader reader = new FileReader(filename)) {
            int charCount = 0;
            int charData;
            while ((charData = reader.read()) != -1) {
                System.out.print((char) charData);
                charCount++;
            }
            System.out.println("\nTotal characters read: " + charCount);
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        App app = new App();
        app.readTextFile("sample.txt");
    }
}

Explanation: FileReader.read() can handle different Unicode characters: For example, the letters in the Arabic word “مرحبا” are returned as single values as follows:

Character: م, Unicode: U+0645  (decimal: 1605)
Character: ر, Unicode: U+0631  (decimal: 1585)
Character: ح, Unicode: U+062D  (decimal: 1581)
Character: ب, Unicode: U+0628  (decimal: 1576)
Character: ا, Unicode: U+0627  (decimal: 1575)

Writing to a file

To write to a file use either FileOutputStream or FileWriter depending on what type of data you want to write. If you’re writing raw bytes, use FileOutputStream. If you want to write stream of characters to a file, use FileWriter.

File Writing with Streams

Writing Raw Bytes (FileOutputStream) Example

In the following example, a string message is converted into a sequence of bytes. These bytes are written to the file using FileOutputStream. Because we are writing the data in ASCII format, each character in the string is converted to its corresponding ASCII byte value before being written to the file. If you open the generated file in any text editor, you will see the original message as plain text since the default character encoding for most text editors is UTF8, which is backward compatible with ASCII.

OnlineGDB ⚡️ - Try it out now GitLab 💻 - View Code

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.FileOutputStream;
import java.io.IOException;

public class App {
    public void writeMessageToFile(String filename, String message) {
        try (FileOutputStream fos = new FileOutputStream(filename)) {
            byte[] messageBytes = message.getBytes();
            fos.write(messageBytes);
            System.out.println("Successfully wrote message to " + filename);
        } catch (IOException e) {
            System.err.println("Error writing file: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        App app = new App();
        String message = "This message will be converted into raw bytes in ASCII format. "+ 
            "These bytes are written to a file using FileOutputStream";
        app.writeMessageToFile("message.bin", message);
    }
}

Writing Characters (FileWriter) Example

In the following example, a string message is written to a file using FileWriter. FileWriter handles character encoding automatically, converting each character in the string to its corresponding byte representation in the specified encoding (UTF-8 by default).

OnlineGDB ⚡️ - Try it out now GitLab 💻 - View Code

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.io.FileWriter;
import java.io.IOException;

public class App {
    public void writeCharacters(String filePath, String content) {
        try (FileWriter writer = new FileWriter(filePath)) {
            writer.write(content);
            System.out.println("Successfully wrote " + content.length() + " characters to file");
        } catch (IOException e) {
            System.err.println("Error writing to file: " + e.getMessage());
        }
    }


    public static void main(String[] args) {
        App app = new App();
        String message = "Hello, مرحبا! 👋 This is a UTF-8 encoded text message.";
        app.writeCharacters("message.txt", message);
    }
}

Buffered Streams

  • Problem: Reading byte-by-byte is slow 🐌 🐢, loading all at once = out of memory error 💥 💣

  • Why?: 💾 Reading from disk for every byte = slow 🐌 🐢

  • What?: 🪣 read chunks of data from disk into a reserved area of memory called buffer memory.

  • How?: Both byte and character streams can be buffered 🚀

    • Byte streams: BufferedInputStream/BufferedOutputStream
    • Character streams: BufferedReader/BufferedWriter

Buffered Streams in Java

Buffered Byte Streams: BufferedInputStream and BufferedOutputStream Example

Below is an example that copies half of the bytes of a binary file (audio file) to a new file using buffered byte streams (BufferedInputStream and BufferedOutputStream).

GitLab 💻 - View Code

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;


public class App {

    public void createShortAudioFile(String inputFilename, String outputFilename) {
        try (FileInputStream fis = new FileInputStream(inputFilename);
             BufferedInputStream bis = new BufferedInputStream(fis);
             FileOutputStream fos = new FileOutputStream(outputFilename);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {

            // Get total bytes in the file to write only half of it
            long totalBytes = new File(inputFilename).length();
            long bytesToCopy = totalBytes / 2;
            
            // Buffer size is 8KB (8192 bytes)
            byte[] buffer = new byte[8192];
            int bytesRead;
            long totalBytesCopied = 0;

            while ((bytesRead = bis.read(buffer)) != -1 && totalBytesCopied < bytesToCopy) {
                if (totalBytesCopied + bytesRead > bytesToCopy) {
                    bytesRead = (int)(bytesToCopy - totalBytesCopied);
                }
                
                bos.write(buffer, 0, bytesRead);
                totalBytesCopied += bytesRead;
            }
            
            System.out.println("Created shorter version (50% smaller): " + totalBytesCopied + " bytes written");
        } catch (IOException e) {
            System.err.println("Error processing file: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        App app = new App();
        app.createShortAudioFile("birds-sound.mp3", "birds-sound-short.mp3");
    }
}

Buffered Character Streams: BufferedReader and BufferedWriter Example

Below is an example that reads the first 1000 lines from the input file (Moby Dick) and writes them to a new file using both buffered readers and writers.

GitLab 💻 - View Code

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

public class App {
    public void copyFirstNLines(String inputFile, String outputFile, int lineCount) {
        try (BufferedReader reader = new BufferedReader(new FileReader(inputFile));
             BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
            
            String line;
            int linesProcessed = 0;
            int charCount = 0;
            
            while ((line = reader.readLine()) != null && linesProcessed < lineCount) {
                writer.write(line);
                writer.newLine();
                charCount += line.length();
                linesProcessed++;
            }
            
            System.out.printf("Copied %d lines (%d characters) to %s%n", 
                            linesProcessed, charCount, outputFile);
            
        } catch (IOException e) {
            System.err.println("Error processing file: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        App app = new App();
        app.copyFirstNLines("MobyDick.txt", "MobyDick-excerpt.txt", 1000);
    }
}

Connecting Streams

Connecting streams in Java involves chaining multiple streams together to achieve more complex I/O operations. This technique allows us to combine the functionality of different streams, such as reading stream of bytes from a file, buffering the input, and then processing the data.

Connecting Streams Example

In this example, we will read a text file using FileInputStream, wrap it with InputStreamReader to handle character encoding (Windows-1256 for Arabic), and then use BufferedReader to buffer the input and read lines efficiently. Finally, we will print each line of the file to the console.

  • Problem: We need to read a text file encoded in Windows-1256 for Arabic, written on a legacy Windows system. Using character streams like Reader or FileReader won’t work because they assume the platform’s default character encoding, which is often UTF-8. Therefore, we need to use byte streams to handle the encoding correctly.
  • Solution: Use FileInputStream to read the raw bytes, InputStreamReader to convert bytes to characters with the specified encoding Windows-1256, and BufferedReader to efficiently read chunks of data and storing them in a buffer to reduce the number of I/O operations.
GitLab 💻 - View Code

arabic-Windows-1256.txt

1
2
3
4
„Õ„œ »‰ „Ê”Ï «·ŒÊ«—“„Ì
»Ê ⁄Û»œ «··Â „ıÕÛ„Û¯œ »‰ „ıÊ”ÛÏ «·ŒÛÊ«—ˆ“„Ì ⁄«·„ —Ì«÷Ì«  Ê›·fl ÊÀ—«›Ì« „”·„.
Ìfl‰Ï »√»Ì Ã⁄›—. fiÌ· √‰Â Ê·œ ÕÊ«·Ì 164‹ 781„ ÊfiÌ· √‰Â  Ê›ÌÛ »⁄œ 232 ‹ √Ì (»⁄œ 847„).
Ì⁄ »— „‰ √Ê«∆· ⁄·„«¡ «·—Ì«÷Ì«  «·„”·„̉ ÕÌÀ ”«Â„  √⁄„«·Â »œÊ— fl»Ì— ›Ì  fiœ„ «·—Ì«÷Ì«  ›Ì ⁄’—Â.

App.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;

public class App {
    public static void main(String[] args) {
        String filePath = "arabic-Windows-1256.txt";

        try (FileInputStream fis = new FileInputStream(filePath);
                InputStreamReader isr = new InputStreamReader(fis, "Windows-1256");
                BufferedReader br = new BufferedReader(isr)) {

            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }
}

Output in UTF-8

1
2
3
4
محمد بن موسى الخوارزمي
بو عَبد الله مُحَمَّد بن مُوسَى الخَوارِزمي عالم رياضيات وفلك وجغرافيا مسلم.
يكنى بأبي جعفر. قيل أنه ولد حوالي 164هـ 781م وقيل أنه توفيَ بعد 232 هـ أي (بعد 847م).
يعتبر من أوائل علماء الرياضيات المسلمين حيث ساهمت أعماله بدور كبير في تقدم الرياضيات في عصره.

Note: In Java 11, the constructor of FileReader can accept [standard charsets supported by the Java platform](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/FileReader.html#%3Cinit%3E(java.io.File,java.nio.charset.Charset). For example, you can use FileReader fr = new FileReader("input.txt", Charset.forName("UTF-8"));. However, for more complex scenarios involving different character encodings, consider using FileInputStream, InputStreamReader and BufferedReader together.


Serialization and Deserialization

  • Serialization: Converting an object into a stream of bytes.
    • Student s -> 📦 -> 01001010
  • Deserialization: Converting a stream of bytes back into an object.
    • 01001010 -> 📭 -> Student s

Serialization in Java Deserialization in Java

Serialization

Serialization is the process of converting an object into a stream of bytes, so it can be saved to a file, database, or transferred over a network.

How to do Serialization in Java

  1. Implement the Serializable interface in the class to be serialized.
  • Create an instance of the class.
  1. Use FileOutputStream to create a file output stream.
  2. Wrap the FileOutputStream with an ObjectOutputStream.
  3. Call the writeObject() method of ObjectOutputStream to serialize the object.
  • Handle exceptions such as IOException.

Serialization Example

To serialize an instance of the Student class into a stream of bytes and save it to a file, we need to ensure that the Student class implements the Serializable interface. This allows the Java serialization mechanism to convert the Student object into a byte stream, which can then be written to a file for persistent storage.

Student.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.Serializable;

class Student implements Serializable {
      private static final long serialVersionUID = 1L;
      private int id;
      private String name;
      private double gpa;

      public Student(int id, String name, double gpa) {
            this.id = id;
            this.name = name;
            this.gpa = gpa;
      }
      // getters and setters and toString
      public int getId() { return id; }
      public void setId(int id) { this.id = id; }
      public String getName() { return name; }
      public void setName(String name) { this.name = name; }
      public double getGpa() { return gpa; }
      public void setGpa(double gpa) { this.gpa = gpa; }
      @Override
      public String toString() {
            return "Student{id=" + id + ", name='" + name + "', gpa=" + gpa + "}";
      }
}

Next, we create an instance of the class and use FileOutputStream to create a file output stream. We wrap the FileOutputStream with an ObjectOutputStream and call the writeObject() to serialize the object.

OnlineGDB ⚡️ - Try it out now GitLab 💻 - View Code

Main.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class Main {
      public static void main(String[] args) {
            Student student = new Student(1, "Fatima Ali", 4.2);
            try (FileOutputStream fileOut = new FileOutputStream("student.dat");
                  ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
                  out.writeObject(student);
                  System.out.println("Serialized student: " + student);
            } catch (Exception e) {
                  System.err.println(e.getMessage());
            }
      }
}

Deserialization

The reverse process of serialization is called deserialization. Deserialization is the process of converting a stream of bytes back into a copy of the original Java object. This allows the object to be reconstructed in memory, making it possible to read data from files, databases, or network sources and use it within your application.

deserialization in Java

Deserialization Example

Similar to serialization, when we want to deserialize a stream of bytes into an object, we will have the Student class implement the Serializable interface. the Next, we create an instance of the class and use FileInputStream to create a file input stream. We wrap the FileInputStream with an ObjectInputStream and call the readObject() to deserialize the byte stream into an object.

OnlineGDB ⚡️ - Try it out now GitLab 💻 - View Code

Main.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Main {
	public static void main(String[] args) {
		try (FileInputStream fileIn = new FileInputStream("student.dat");
			        ObjectInputStream in = new ObjectInputStream(fileIn)) {
			Student student = (Student) in.readObject();
			System.out.println("Deserialized student: " + student);
		} catch (Exception e) {
			System.err.println(e.getMessage());
		}
	}
}

Conclusion

Java’s I/O API may initially seem complex compared to some programming languages like Python and Ruby. However, many other languages, such as C++ and C#, share similar concepts for handling I/O and working with streams of bytes and characters. Once the core concepts and interactions between different classes are understood, working with Java’s I/O API becomes much more easier.