Java Programming

Java I/O, Files, Byte and Character Streams and Serialization

PGCP-BDA

byte stream

An InputStream or OutputStream that transfers raw eight-bit bytes and is suitable for binary data.

character stream

A Reader or Writer that transfers Unicode characters using an encoding when bytes cross an external boundary.

InputStream and OutputStream

The abstract base classes for reading and writing byte streams in Java.

Reader and Writer

The abstract base classes for reading and writing character streams in Java.

buffering

Temporary in-memory accumulation that reduces expensive underlying I/O operations and may require an explicit flush.

Path and Files

Path represents a filesystem location, while Files supplies static operations for reading, writing, copying and inspecting it.

Path represents a filesystem path, while Files performs operations:

Path source = Path.of("data", "input.txt");
Path target = Path.of("backup", "input.txt");

Files.createDirectories(target.getParent());
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

Useful operations include exists, isDirectory, size, readString, writeString, copy, move, deleteIfExists, list and walk. Streams returned by methods such as Files.list and Files.walk hold resources and should be used in try-with-resources.

File is the older pathname abstraction. Constructing either a File or Path object does not create a filesystem entry. Relative paths are resolved against the process's current working directory.

serialization

Java serialization converts a supported object graph into a byte representation, but requires version, security, identity and compatibility considerations.

serialVersionUID

A class version identifier used during Java deserialization to check compatibility with serialized object data.

resource management

Acquiring limited resources for the shortest required scope and reliably releasing them on success or failure.

Character streams and encodings

Reader and Writer operate on characters. Encoding translates between characters and bytes. The same encoding must be understood at both ends:

Path path = Path.of("message.txt");
try (BufferedWriter writer =
         Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
    writer.write("नमस्ते Java");
}

InputStreamReader decodes bytes into characters, while OutputStreamWriter encodes characters into bytes. Avoid platform-default encoding when a file or protocol needs portable behaviour. UTF-8 is a common explicit choice.

BufferedReader.readLine() returns a line without its line terminator or null at end of input. PrintWriter offers convenient formatted text output but some methods record errors rather than throwing them, so check its contract when error detection matters.

Byte streams

InputStream and OutputStream are abstract bases for binary I/O. Use them for images, archives, audio, protocol messages and any data whose meaning is bytes.

try (InputStream in = new BufferedInputStream(
         Files.newInputStream(Path.of("photo.jpg")));
     OutputStream out = new BufferedOutputStream(
         Files.newOutputStream(Path.of("photo-copy.jpg")))) {
    in.transferTo(out);
}

read() returns an int so it can represent byte values 0–255 and -1 for end of stream. Bulk reads may return fewer bytes than requested, so code must process the actual count. Never convert arbitrary binary data through a character encoding.

Buffering and stream wrapping

Physical I/O calls can be expensive. A buffered wrapper accumulates data so many small application operations require fewer underlying operations. Wrappers form a pipeline:

try (DataInputStream input = new DataInputStream(
         new BufferedInputStream(
             Files.newInputStream(path)))) {
    int id = input.readInt();
    double amount = input.readDouble();
}

Close the outermost wrapper; its close operation normally closes the wrapped resource according to its contract. A larger buffer is not automatically faster and buffering does not change the logical data format.

Try-with-resources

Try-with-resources manages objects implementing AutoCloseable:

try (BufferedReader reader = Files.newBufferedReader(
        Path.of("notes.txt"), StandardCharsets.UTF_8)) {
    System.out.println(reader.readLine());
}

close() runs automatically after the body, including when the body throws. Resources are closed in reverse declaration order:

try (InputStream in = Files.newInputStream(source);
     OutputStream out = Files.newOutputStream(target)) {
    in.transferTo(out);
}

Here out closes before in. Java 9 and later can also use an already-declared final or effectively final resource variable in the resource header.

Data streams and object streams

DataOutputStream writes primitive values in a defined binary representation and DataInputStream reads them. Writer and reader must use the same field order and types.

ObjectOutputStream and ObjectInputStream encode and restore supported Java object graphs. They are different from data streams: object streams include type and graph information and preserve repeated-reference relationships within the stream.

Do not treat Java object serialization as a general long-term interchange format. It couples data to Java types and version details. JSON, a schema-based binary format or explicit records are often better for external boundaries.

Default Java serialization

A class opts into ordinary serialization by implementing the marker interface Serializable:

class SessionData implements Serializable {
    private static final long serialVersionUID = 1L;
    private String userName;
    private transient String temporaryToken;
}

Default serialization records eligible non-static, non-transient instance state through the serializable object graph. Static fields belong to the class rather than an individual object and are not ordinary serialized state. A transient field receives its default value after deserialization unless custom logic reconstructs it.

serialVersionUID declares a version identifier used during compatibility checking. Declaring it avoids relying on a compiler-generated value that can change when the class changes.

Reliable I/O design

Keep parsing separate from transport. One method can obtain text, another can validate and convert it and domain code can operate on typed values. This division makes errors clearer and tests easier.

At system boundaries:

  • validate paths and input sizes;
  • choose and document the character encoding;
  • avoid logging secrets or entire sensitive files;
  • write important files atomically when partial output would be harmful;
  • wrap low-level exceptions only when the new exception adds meaningful context;
  • retain the original cause.

Serialization construction and customization

Deserialization does not create a serializable class by invoking its ordinary constructor. The first non-serializable superclass must have an accessible no-argument constructor and its portion is initialized through that constructor. Serializable subclass fields are restored from the stream.

Private writeObject and readObject methods can customize default serialization, validate restored state or reconstruct transient data. Such code must preserve class invariants. Deserialization callbacks and object creation make untrusted native serialization dangerous; accept only controlled data with strict filters or use a safer format.

Throwing and declaring

throw transfers control using one exception object:

if (amount <= 0) {
    throw new IllegalArgumentException("amount must be positive");
}

throws appears in a method declaration and states that a failure may propagate:

String load(Path path) throws IOException {
    return Files.readString(path);
}

A method can declare several exception types. Declaring throws Exception is usually too broad for an application API because callers cannot tell which failures are expected. A clear exception type and message should describe the failed operation and relevant safe context.

NIO channels and buffers

Channels transfer data to or from buffers. A ByteBuffer has a capacity, position and limit. A common read cycle is:

  1. read bytes from a channel into the buffer;
  2. call flip() to set the limit to the written position and position to zero;
  3. consume bytes while hasRemaining();
  4. call clear() or compact() before the next read.
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) != -1) {
    buffer.flip();
    while (buffer.hasRemaining()) {
        consume(buffer.get());
    }
    buffer.clear();
}

clear() resets indexes but does not erase bytes. Channels support features such as seeking, file locking, memory mapping and non-blocking network operations depending on the channel type.

Shallow and deep copying

A shallow copy creates a new outer object but shares referenced nested objects:

original Employee ──► Address
copied Employee   ──► same Address

A deep copy duplicates selected nested state:

original Employee ──► Address A
copied Employee   ──► Address B

The business model must decide what to share. Immutable values may safely remain shared; mutable owned state may require duplication; entities with independent identity should not be copied blindly. Serialization round-tripping is not a universal deep-copy definition and introduces failure, performance, security and versioning concerns. Prefer explicit copy constructors or factory methods that state the policy.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.