1. I/O Basics

Input/Output (I/O) in Java refers to the process of reading data from a source (input) and writing data to a destination (output). Java provides a robust I/O framework to handle various data sources and destinations, such as files, console, network sockets, and memory.

  • Key Concepts:

    • I/O operations are performed using streams, which are sequences of data.
    • Java I/O is built around the java.io package, with additional support in java.nio (New I/O) for advanced operations.
    • I/O operations often involve exceptions (e.g., IOException), requiring proper handling.
  • Types of I/O:

    • Byte-oriented I/O: Handles raw binary data (e.g., images, files) using byte streams.
    • Character-oriented I/O: Handles text data (e.g., Unicode characters) using character streams.
    • Buffered I/O: Uses buffers to reduce direct access to underlying resources, improving performance.
    • Non-buffered I/O: Direct access to resources, less efficient for frequent operations.
  • Why Important?:

    • Enables interaction with external systems (files, network, console).
    • Supports data persistence and communication.
    • Critical for real-world applications (e.g., reading configuration files, logging).

2. Streams and Stream Classes

Streams are abstractions for reading from or writing to a data source/destination. Java provides two main types of streams: byte streams and character streams, each with specific classes in the java.io package.

  • Byte Streams:

    • Handle raw binary data (8-bit bytes).
    • Base classes:
      • InputStream: Abstract class for reading bytes.
      • OutputStream: Abstract class for writing bytes.
    • Common subclasses:
      • FileInputStream, FileOutputStream: For file I/O.
      • BufferedInputStream, BufferedOutputStream: For buffered I/O.
      • DataInputStream, DataOutputStream: For reading/writing primitive data types.
      • ObjectInputStream, ObjectOutputStream: For object serialization.
  • Character Streams:

    • Handle Unicode characters (16-bit).
    • Base classes:
      • Reader: Abstract class for reading characters.
      • Writer: Abstract class for writing characters.
    • Common subclasses:
      • FileReader, FileWriter: For file I/O.
      • BufferedReader, BufferedWriter: For buffered I/O.
      • InputStreamReader, OutputStreamWriter: Bridge between byte and character streams.
  • Key Features:

    • Chaining: Streams can be wrapped (e.g., BufferedReader wraps FileReader for efficiency).
    • Closing: Streams must be closed to release resources (use close() or try-with-resources).
    • Exceptions: Most I/O operations throw IOException (checked exception).
  • Example (Byte Stream):

    package com.example.myapp;
    import java.io.*;
    public class ByteStreamDemo {
        public static void main(String[] args) {
            try (FileInputStream fis = new FileInputStream("input.txt");
                 FileOutputStream fos = new FileOutputStream("output.txt")) {
                int byteData;
                while ((byteData = fis.read()) != -1) { // Read byte
                    fos.write(byteData); // Write byte
                }
            } catch (IOException e) {
                System.out.println("Error: " + e.getMessage());
            }
        }
    }
  • Example (Character Stream):

    package com.example.myapp;
    import java.io.*;
    public class CharStreamDemo {
        public static void main(String[] args) {
            try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
                 BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    writer.write(line);
                    writer.newLine();
                }
            } catch (IOException e) {
                System.out.println("Error: " + e.getMessage());
            }
        }
    }
  • Key Points:

    • Use byte streams for binary data, character streams for text.
    • Buffering (e.g., BufferedReader) improves performance.
    • Try-with-resources (Java 7+) ensures streams are closed automatically.

3. The Predefined Streams

Java provides three predefined streams in the java.lang.System class for console I/O, automatically available in every program.

  • System.in:

    • Type: InputStream.
    • Purpose: Reads input from the console (standard input, typically the keyboard).
    • Example: Use with Scanner or BufferedReader.
  • System.out:

    • Type: PrintStream.
    • Purpose: Writes output to the console (standard output).
    • Example: System.out.println("Hello").
  • System.err:

    • Type: PrintStream.
    • Purpose: Writes error messages to the console (standard error).
    • Example: System.err.println("Error occurred").
  • Example:

    package com.example.myapp;
    import java.io.*;
    public class PredefinedStreamDemo {
        public static void main(String[] args) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            try {
                System.out.println("Enter your name: ");
                String name = reader.readLine();
                System.out.println("Hello, " + name);
                System.err.println("This is an error message");
            } catch (IOException e) {
                System.err.println("Error reading input: " + e.getMessage());
            }
        }
    }
  • Key Points:

    • System.in is typically wrapped with BufferedReader or Scanner for easier input.
    • System.out and System.err are PrintStream objects, supporting methods like println() and printf().
    • These streams are automatically open and do not need closing.

4. Reading from and Writing to Console

Console I/O involves reading user input from the keyboard (System.in) and writing output to the console (System.out, System.err).

  • Reading from Console:

    • Use Scanner (simpler) or BufferedReader (more control).
    • Example with Scanner:
      package com.example.myapp;
      import java.util.Scanner;
      public class ConsoleReadDemo {
          public static void main(String[] args) {
              Scanner scanner = new Scanner(System.in);
              System.out.println("Enter an integer: ");
              try {
                  int number = scanner.nextInt();
                  System.out.println("You entered: " + number);
              } catch (Exception e) {
                  System.err.println("Invalid input: " + e.getMessage());
              } finally {
                  scanner.close();
              }
          }
      }
    • Example with BufferedReader:
      package com.example.myapp;
      import java.io.*;
      public class ConsoleReadBufferedDemo {
          public static void main(String[] args) {
              try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
                  System.out.println("Enter text: ");
                  String text = reader.readLine();
                  System.out.println("You entered: " + text);
              } catch (IOException e) {
                  System.err.println("Error: " + e.getMessage());
              }
          }
      }
  • Writing to Console:

    • Use System.out.println(), System.out.printf(), or System.out.print().
    • Example:
      System.out.println("Simple message");
      System.out.printf("Formatted: %d, %s%n", 42, "test");
      System.err.println("Error message");
  • Key Points:

    • Scanner is easier for parsing numbers, strings, etc.
    • BufferedReader is better for reading raw text or large inputs.
    • Always handle IOException for BufferedReader and validate Scanner input.
    • Use try-with-resources to close Scanner or BufferedReader.

5. Reading and Writing Files

File I/O involves reading from and writing to files using byte or character streams. Java provides classes like FileInputStream, FileOutputStream, FileReader, and FileWriter, often wrapped with buffered streams for efficiency.

  • Reading a File:

    • Use FileReader with BufferedReader for text files.
    • Use FileInputStream with BufferedInputStream for binary files.
    • Example (Text File):
      package com.example.myapp;
      import java.io.*;
      public class FileReadDemo {
          public static void main(String[] args) {
              try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
                  String line;
                  while ((line = reader.readLine()) != null) {
                      System.out.println(line);
                  }
              } catch (IOException e) {
                  System.err.println("Error reading file: " + e.getMessage());
              }
          }
      }
  • Writing to a File:

    • Use FileWriter with BufferedWriter for text files.
    • Use FileOutputStream with BufferedOutputStream for binary files.
    • Example (Text File):
      package com.example.myapp;
      import java.io.*;
      public class FileWriteDemo {
          public static void main(String[] args) {
              try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
                  writer.write("Hello, Java!");
                  writer.newLine();
                  writer.write("This is a test.");
              } catch (IOException e) {
                  System.err.println("Error writing file: " + e.getMessage());
              }
          }
      }
  • Key Points:

    • Use try-with-resources to ensure streams are closed.
    • Handle IOException (e.g., FileNotFoundException for missing files).
    • Use newLine() for platform-independent line breaks in character streams.
    • For appending to a file, use FileWriter(file, true) (append mode).
  • File Class:

    • The java.io.File class represents file/directory paths.
    • Methods: exists(), isFile(), isDirectory(), createNewFile(), delete().
    • Example:
      File file = new File("data.txt");
      if (file.exists()) {
          System.out.println("File size: " + file.length());
      }

6. The Transient and Volatile Modifiers

The transient and volatile modifiers are related to I/O in the context of serialization and multithreading, respectively.

  • Transient Modifier:

    • Used in serialization to mark fields that should not be serialized (saved) when an object is written to a stream.
    • Serialization: Converting an object to a byte stream (e.g., using ObjectOutputStream).
    • Example:
      package com.example.myapp;
      import java.io.*;
      public class TransientDemo implements Serializable {
          private String name;
          private transient String password; // Not serialized
          public TransientDemo(String name, String password) {
              this.name = name;
              this.password = password;
          }
          public static void main(String[] args) {
              TransientDemo obj = new TransientDemo("Alice", "secret");
              // Serialize
              try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"))) {
                  oos.writeObject(obj);
              } catch (IOException e) {
                  System.err.println("Error: " + e.getMessage());
              }
              // Deserialize
              try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"))) {
                  TransientDemo deserialized = (TransientDemo) ois.readObject();
                  System.out.println("Name: " + deserialized.name); // Alice
                  System.out.println("Password: " + deserialized.password); // null
              } catch (IOException | ClassNotFoundException e) {
                  System.err.println("Error: " + e.getMessage());
              }
          }
      }
  • Volatile Modifier:

    • Used in multithreading to ensure a variable’s value is always read from and written to main memory, preventing thread-local caching.
    • Ensures visibility of changes across threads but does not guarantee atomicity.
    • Example:
      package com.example.myapp;
      public class VolatileDemo {
          private volatile boolean running = true;
          public void stop() {
              running = false;
          }
          public void run() {
              while (running) {
                  System.out.println("Running...");
                  try {
                      Thread.sleep(500);
                  } catch (InterruptedException e) {
                      System.err.println("Interrupted");
                  }
              }
              System.out.println("Stopped");
          }
          public static void main(String[] args) throws InterruptedException {
              VolatileDemo demo = new VolatileDemo();
              Thread t1 = new Thread(demo::run);
              t1.start();
              Thread.sleep(2000);
              demo.stop();
          }
      }
  • Key Points:

    • transient: Prevents sensitive data (e.g., passwords) from being serialized.
    • volatile: Ensures thread visibility for variables in multithreaded environments.
    • Use transient in I/O (serialization), volatile in multithreading (with I/O implications for shared resources).

7. Using Instance of Native Methods

Native methods are Java methods implemented in a non-Java language (e.g., C or C++) using the Java Native Interface (JNI). They are used for performance-critical tasks or to access platform-specific features not available in Java.

  • Key Points:

    • Declared with the native keyword and no implementation in Java.
    • Implemented in a native library (e.g., .dll on Windows, .so on Linux).
    • Loaded using System.loadLibrary().
  • Syntax:

    public native void nativeMethod();
  • Example:

    package com.example.myapp;
    public class NativeDemo {
        public native void printHello();
        static {
            System.loadLibrary("NativeLib"); // Load native library
        }
        public static void main(String[] args) {
            NativeDemo demo = new NativeDemo();
            demo.printHello(); // Calls native method
        }
    }
    • C Implementation (e.g., NativeLib.c):
      #include <jni.h>
      #include <stdio.h>
      JNIEXPORT void JNICALL Java_com_example_myapp_NativeDemo_printHello(JNIEnv *env, jobject obj) {
          printf("Hello from C!\n");
      }
    • Compile and link the C code into a shared library (platform-specific).
  • Steps to Use Native Methods:

    1. Declare the native method in Java.
    2. Generate a header file using javah (or javac -h in Java 9+).
    3. Implement the method in C/C++.
    4. Compile the C/C++ code into a shared library.
    5. Load the library with System.loadLibrary().
    6. Call the native method.
  • Key Points:

    • Native methods are rare in standard I/O but used in libraries like java.io for low-level file operations.
    • Risks: Platform dependence, memory leaks, and debugging complexity.
    • Requires CLASSPATH to include native libraries.
  • Connection to I/O:

    • Native methods are used in java.io classes (e.g., FileInputStream) for OS-level file access.
    • Example: FileInputStream.read() may call native code for direct disk access.

Practice Program:

package com.example.myapp;
import java.io.*;
import java.util.Scanner;
public class IODemo implements Serializable {
    private String data;
    private transient int temp;
    public IODemo(String data, int temp) {
        this.data = data;
        this.temp = temp;
    }
    public static void main(String[] args) {
        // Console I/O
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter text: ");
        String input = scanner.nextLine();
        System.out.println("You entered: " + input);
        scanner.close();
        // File I/O
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
            writer.write(input);
        } catch (IOException e) {
            System.err.println("Error writing file: " + e.getMessage());
        }
        // Serialization
        IODemo obj = new IODemo("Test", 42);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"))) {
            oos.writeObject(obj);
        } catch (IOException e) {
            System.err.println("Error serializing: " + e.getMessage());
        }
    }
}

Compile and Run:

javac com/example/myapp/IODemo.java
java -cp . com.example.myapp.IODemo