Programming in Java

Chapter 6
File Systems, I/O, Serialization, and Data Formats

Post Graduate Program in Computer Science Engineering
Programming in Java   •   Post Graduate

6. File Systems, I/O, Serialization, and Data Formats

Introduction and Context

Software rarely operates only on values held in memory. Business transactions, configuration files, logs, images, reports, machine-learning datasets, and messages exchanged between services must be stored or transmitted. Java therefore provides a broad set of abstractions for interacting with files, devices, networks, and structured data. The central design challenge is not merely how to read or write bytes, but how to do so safely, efficiently, portably, and in a form that remains useful to other systems.

Java’s traditional `java.io` package introduced streams, readers, writers, buffering, and serialization. The NIO.2 APIs in `java.nio.file` later provided a richer model of paths, file attributes, directory traversal, symbolic links, file-system providers, and asynchronous operations. Modern applications also use JSON and XML libraries, database formats, object mapping, and safer serialization alternatives. These mechanisms serve different purposes. A byte stream is appropriate for an image; a character reader is appropriate for text; a channel may be preferable for large or random-access data; JSON is often suitable for service interchange; and native Java serialization is generally unsuitable for untrusted input.

This chapter develops a coherent model for selecting and using these mechanisms. It emphasizes resource safety, character encoding, failure handling, security, performance, and interoperability rather than treating file I/O as a collection of isolated library calls.

Learning Objectives

After studying this chapter, the reader should be able to:

  • distinguish byte-oriented, character-oriented, buffered, channel-based, and asynchronous I/O;
  • use `Path`, `Files`, and NIO.2 operations for portable file-system programming;
  • explain the roles of buffering, character sets, file attributes, and resource ownership;
  • select between streams, channels, memory mapping, and asynchronous I/O for different workloads;
  • serialize and deserialize data using suitable JSON, XML, binary, or database-oriented formats;
  • identify the security risks of native Java serialization, path manipulation, symbolic links, and uncontrolled resource use;
  • design robust data-processing components with validation, atomic updates, bounded memory, and meaningful error handling.

The I/O Model: Data, Resources, and Boundaries

An I/O operation crosses a boundary between a program and an external resource. The resource may be a regular file, terminal, pipe, socket, device, or virtual file system. Unlike an object in memory, an external resource can disappear, become inaccessible, return partial data, or fail because of permissions, capacity, encoding, or concurrent modification.

A stream presents data as a sequential flow. An input stream supplies data to a program, whereas an output stream accepts data from it. The stream abstraction hides many device-specific details and allows the same processing pattern to be used with files, network connections, and in-memory buffers. A stream does not imply that all data is available at once; it is particularly valuable when processing large or continuously produced data.

A channel is a more general NIO abstraction. It can support reading, writing, positioning, transfer operations, and interaction with buffers. A channel normally works with a `ByteBuffer`, which explicitly represents a region of memory and maintains position, limit, and capacity. This makes data movement more visible and can support high-throughput or random-access operations.

A resource has a lifecycle. It is acquired, used, and released. File descriptors, sockets, streams, readers, writers, and channels should not remain open after their work is complete. Java’s try-with-resources statement expresses this lifecycle directly:

```java

try (BufferedReader reader = Files.newBufferedReader(

Path.of("records.csv"), StandardCharsets.UTF_8)) {

String line;

while ((line = reader.readLine()) != null) {

process(line);

}

}

```

The resource is closed even when processing throws an exception. Closing an outer wrapper normally closes the underlying resource as well. This ownership relationship should be clear: the component that opens a resource should normally be responsible for closing it, unless ownership is explicitly transferred.

Bytes, Characters, and Encodings

A byte is an eight-bit unit used by files and communication mechanisms. A character is an abstract symbol, such as a Latin letter, mathematical sign, or emoji. An encoding maps characters to bytes. UTF-8 is widely used because it represents all Unicode characters, preserves ASCII compatibility, and uses one to four bytes per character.

`InputStream` and `OutputStream` operate on bytes. They are appropriate for binary content, compressed data, cryptographic material, and encoded text when the program deliberately manages encoding. `Reader` and `Writer` operate on characters and are appropriate when the application is processing text. The conversion between bytes and characters should specify a charset rather than depend on the operating system’s default:

```java

try (BufferedReader reader = Files.newBufferedReader(

path, StandardCharsets.UTF_8);

BufferedWriter writer = Files.newBufferedWriter(

output, StandardCharsets.UTF_8,

StandardOpenOption.CREATE,

StandardOpenOption.TRUNCATE_EXISTING)) {

String line;

while ((line = reader.readLine()) != null) {

writer.write(line.toUpperCase(Locale.ROOT));

writer.newLine();

}

}

```

An omitted charset can cause the same file to be interpreted differently on different systems. This is a portability defect, not merely a formatting concern. Applications should also decide how malformed input is handled: reject it, replace invalid sequences, or report it for later correction. Silent replacement may hide corruption.

The methods `readAllBytes()` and `readString()` are convenient for small files but load the complete content into memory. For large files, use a streaming loop, buffered processing, or a parser designed for incremental input. A line-oriented reader is not always sufficient: a logical record may contain embedded newlines, and blindly splitting text may corrupt quoted formats such as CSV.

Traditional Streams and Buffering

The `java.io` hierarchy contains several important abstractions. `FileInputStream` and `FileOutputStream` handle bytes. `FileReader` and `FileWriter` provide character access, although explicit NIO charset APIs are usually preferable. `BufferedInputStream`, `BufferedOutputStream`, `BufferedReader`, and `BufferedWriter` reduce the number of expensive underlying operations by accumulating data in memory.

Buffering improves performance because operating-system calls and storage access have greater overhead than copying data within memory. It does not change the logical content and does not guarantee durability. A writer may have data in a Java buffer, an operating-system cache, or a storage device cache. `flush()` transfers data from a writer’s buffer to its underlying stream; it does not necessarily guarantee that the data has reached stable storage. When durability matters, applications may need `FileChannel.force(true)` and an appropriate storage strategy.

The `DataInputStream` and `DataOutputStream` classes read and write primitive values in a defined binary representation. They are useful for controlled binary protocols, but the resulting format is not automatically self-describing or interoperable. The writer and reader must agree on field order, numeric representation, versioning, and character encoding.

The `ObjectInputStream` and `ObjectOutputStream` classes support native Java object serialization. Although convenient for tightly controlled legacy systems, native serialization embeds class identity and implementation details and can execute deserialization-related behavior. It should not be used to process untrusted data. Even in trusted environments, long-term persistence is fragile because changes to class definitions can make old data difficult to read.

NIO.2: Paths, Files, and File-System Operations

NIO.2 models a location with `Path`. A `Path` is not the file itself; it is a representation of a file-system path interpreted by a provider. Paths can be created with `Path.of()` or `Paths.get()`, combined with `resolve()`, normalized, relativized, and converted between absolute and relative forms.

The `Files` class supplies operations for creating, copying, moving, deleting, reading, writing, querying, and traversing files. Examples include `Files.exists`, `Files.isRegularFile`, `Files.createDirectories`, `Files.copy`, `Files.move`, and `Files.deleteIfExists`. Most operations throw checked exceptions such as `IOException`, requiring the application to make an explicit decision about recovery, reporting, or propagation.

A robust application distinguishes a missing file from a permission failure, malformed data, and temporary unavailability. Catching every exception and returning an empty result destroys this distinction and can cause data loss. Error messages should include useful context without disclosing secrets or exposing sensitive file-system details to untrusted users.

Directory traversal can be performed with `DirectoryStream`, `Files.list`, or `Files.walk`. These APIs may hold resources open and should themselves be used in try-with-resources where applicable:

```java

try (Stream<Path> entries = Files.walk(root)) {

entries.filter(Files::isRegularFile)

.filter(p -> p.toString().endsWith(".log"))

.forEach(this::archive);

}

```

A stream returned by a file-system traversal is lazy and may encounter an exception during terminal processing. Resource closure and exception handling therefore remain necessary. For very large directory trees, consider filtering early, bounding traversal, and avoiding collection of every path into memory.

File Attributes and Symbolic Links

NIO.2 supports basic attributes and provider-specific views such as POSIX, DOS, ACL, and user-defined attributes. Metadata can include size, timestamps, file type, permissions, and ownership. A program that relies on permissions or modification times should recognize that attributes vary across operating systems and providers.

Symbolic links introduce a security and correctness issue. An operation on a path may follow a link and reach a location outside the intended directory. Options such as `NOFOLLOW_LINKS` can prevent certain forms of link following, but they do not by themselves solve every time-of-check-to-time-of-use race. Secure file handling should minimize the gap between validation and use, restrict the process’s privileges, and use operating-system mechanisms appropriate to the threat model.

Channels, Buffers, and Asynchronous I/O

A channel-based design separates the data container from the operation that fills or drains it. A typical loop reads from a `FileChannel` into a `ByteBuffer`, switches the buffer from writing mode to reading mode with `flip()`, processes the bytes, and prepares it for another read with `clear()` or `compact()`.

```java

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {

ByteBuffer buffer = ByteBuffer.allocate(8192);

while (channel.read(buffer) != -1) {

buffer.flip();

while (buffer.hasRemaining()) {

consume(buffer.get());

}

buffer.clear();

}

}

```

A buffer’s `position` identifies the next location for reading or writing, `limit` marks the boundary of meaningful data, and `capacity` is its total size. Confusing these states is a common source of bugs. `flip()` sets the limit to the current position and resets the position to zero; `clear()` prepares the entire buffer for new input but does not erase its old bytes.

`FileChannel` supports positional reads and writes, truncation, file locks, transfers, and memory mapping. Memory-mapped files can be effective for large, random-access data because file regions are mapped into virtual memory. They also complicate resource and capacity management, and they are not automatically the best choice for sequential processing.

Asynchronous file channels allow operations to complete through a `Future` or a completion handler. This can improve responsiveness in servers that must manage many concurrent operations, but asynchronous APIs do not eliminate storage latency or make a poor workload efficient. They introduce concerns about executor configuration, cancellation, ordering, back-pressure, and exception propagation. An application should use asynchronous I/O when its concurrency model benefits from it, not merely because it is newer.

Serialization and Data Representation

Serialization is the conversion of an in-memory representation into a form that can be stored or transmitted. Deserialization reconstructs a usable representation. A useful serialization format should address at least five concerns: representation, schema, compatibility, validation, and security.

JSON is human-readable, widely supported, and natural for hierarchical objects. It is commonly used in REST services and configuration. A JSON library can map data to records, classes, maps, or streaming tokens. Mapping directly into domain objects is convenient, but input must still be validated. Unknown fields, missing fields, numeric ranges, dates, and polymorphic types require explicit policy.

XML supports namespaces, attributes, mixed content, and mature schema and transformation tools. It remains important in enterprise integration and document-oriented systems. XML processing must disable unsafe external entity resolution when external entities are not required. Otherwise, an attacker may exploit external resource access or entity expansion. Parser configuration is therefore part of secure application design.

Delimited text such as CSV is simple only when its grammar is respected. Quoting, escaping, line endings, embedded separators, and character encoding must be handled by a proper parser rather than by a naïve `split()` call. Binary formats can be compact and fast, but they require stronger schema governance and tooling. Protocol Buffers, Avro, CBOR, and similar formats can support explicit schemas, efficient representation, and compatibility rules.

Java records can be useful as immutable data-transfer structures when mapping external data. A record does not automatically validate its components or make deserialization safe. Validation belongs at the boundary, and domain invariants should be enforced before data is used in sensitive operations.

Compatibility and Schema Evolution

A format used beyond a single method call needs a compatibility strategy. Adding an optional field may be backward-compatible, while renaming or changing the meaning of an existing field may not be. Numeric overflow, date-format changes, locale assumptions, and changes in nullability can break consumers even when the syntax remains valid.

Version fields, explicit schemas, tolerant readers, migration programs, and contract tests are common techniques. Compatibility should be evaluated in both directions: can a new reader consume old data, and can an old reader consume new data? The answer depends on the format and the chosen evolution rules.

Safe File-Processing Architecture

A reliable data-processing component can be understood as a sequence of boundaries:

1. Locate the resource using a controlled path or resource identifier.

2. Authorize the operation and verify that the caller may access it.

3. Open the resource with explicit options, charset, and limits.

4. Decode bytes into records using a defined format.

5. Validate syntax, schema, size, ranges, and business rules.

6. Transform data while preserving required invariants.

7. Commit output atomically or through a recoverable transaction strategy.

8. Close and audit resources, outcomes, and relevant failures.

This sequence prevents an important category error: treating syntactic validity as semantic validity. A JSON document may be well formed but contain an invalid account identifier. A path may be syntactically legal but escape an authorized directory. A file may be readable but too large for safe processing.

For replacement of an important file, a safer approach is often to write a temporary file in the same directory, flush and close it, and then move it into place using an atomic move where supported. The exact guarantees depend on the file system and provider. A backup or journal may be required when recovery from interruption is important.

Applied Case: A Secure Document-Import Service

Background

Consider an illustrative case involving a university research portal. Researchers upload JSON metadata and associated PDF documents. The service stores the files, indexes selected fields, and makes approved documents available to authenticated users.

Situation and Problem

The initial implementation accepted a filename from an HTTP request, concatenated it with an upload directory, called `readAllBytes()`, and deserialized the result directly into a domain object. It used the platform default charset for text logs. Under load, memory consumption increased sharply. A malicious filename could attempt directory traversal, and malformed JSON could trigger unhandled exceptions. A later review also found that uploaded files could overwrite existing files.

Analysis

The problems occurred at several boundaries. The path was influenced by an external party and was not confined securely. The complete-file read imposed an unbounded memory cost. Deserialization was treated as validation, although the JSON structure did not establish that identifiers, sizes, or access classifications were valid. Finally, file replacement was not atomic, and operational logs lacked correlation identifiers.

Approach and Solution

The revised design assigns each upload a server-generated identifier and does not use the submitted filename as a storage path. It resolves the destination beneath an authorized directory, rejects unexpected path forms, applies maximum size limits, and checks the resulting file type according to the application’s policy. The service writes to a newly created temporary file using try-with-resources and moves it into the final location only after successful validation.

JSON is parsed with a maintained library into a small input record. Required fields, maximum string lengths, allowed enumerations, and business rules are validated before a domain object is created. Large documents are streamed or copied with bounded buffers rather than loaded wholly into memory. The service uses UTF-8 explicitly for metadata and writes structured audit events that contain the upload identifier, outcome, and reason for rejection without recording confidential document contents.

Authorization is checked before retrieval, and response headers are selected to prevent unintended browser interpretation. The service uses least-privilege file-system permissions and separates temporary, quarantined, and approved storage areas. Automated tests cover malformed JSON, oversized input, traversal attempts, interrupted writes, concurrent access, and platform-specific path behavior.

Outcome

The revised service has predictable memory use, clearer failure semantics, and stronger separation between untrusted input and internal storage. Atomic replacement reduces the likelihood that readers observe a partially written file. The design also makes future format migration easier because parsing, validation, storage, and domain logic are separate components.

Learning Points

The case demonstrates that file I/O is an architectural concern. Correctness depends on the relationship among paths, resources, encodings, parsers, validation, authorization, and failure recovery. A shorter implementation is not necessarily a simpler or safer implementation.

Risks, Limitations, and Professional Responsibilities

Common mistakes include omitting a charset, closing only one resource in a complex chain, assuming `available()` reports the total file size, ignoring partial reads, using `readAllBytes()` for uncontrolled input, treating file extensions as proof of content type, and catching `Exception` without preserving diagnostic context.

Path traversal is possible when user-controlled path fragments are concatenated without policy enforcement. Race conditions can arise when a program checks a file and later opens it, assuming its properties have not changed. Symbolic links, shared directories, and concurrent processes make this assumption unsafe. Applications handling sensitive data should use dedicated directories, least-privilege accounts, restrictive permissions, and platform-supported secure creation operations.

Deserialization is an attack surface. Native Java serialization should generally be avoided for untrusted data; if legacy compatibility requires it, apply strict filtering, isolate the operation, authenticate the source, and constrain types and resource consumption. JSON and XML are not automatically safe merely because they are text. Parser configuration, validation, size limits, and protection against resource-exhaustion attacks remain necessary.

Privacy and governance also matter. Files may contain personal, confidential, or regulated information. Retention periods, access logs, encryption at rest and in transit, secure deletion policies, backup controls, and data-minimization requirements should be defined by the organization’s legal and governance context. Logs must support accountability without becoming an uncontrolled copy of sensitive data.

Contemporary Directions and Best Practices

Cloud object stores, containerized deployments, network file systems, and virtual file-system providers challenge assumptions that a path identifies a local spinning disk. Applications should define consistency and durability requirements explicitly. A cloud object may provide different rename, locking, and atomicity semantics from a local file.

Reactive pipelines and structured concurrency can help coordinate large numbers of I/O tasks, but they do not remove the need for bounded queues and back-pressure. Observability increasingly includes operation duration, bytes processed, failure categories, retry counts, and resource utilization. Metrics should be designed so that abnormal behavior can be detected without exposing data contents.

Practical best practices include:

  • specify character sets explicitly;
  • use try-with-resources for every closeable resource;
  • stream large data and impose size, depth, and time limits;
  • separate parsing, validation, transformation, and persistence;
  • use schema-aware formats and document compatibility rules;
  • avoid native Java serialization for untrusted or long-lived data;
  • constrain paths and avoid trusting filenames supplied by users;
  • prefer atomic or transactional update strategies for important files;
  • test failure, interruption, concurrency, encoding, and platform variation;
  • monitor I/O latency, throughput, errors, and resource consumption.

Chapter Summary

Java provides several complementary approaches to external data. Byte streams handle binary data, while readers and writers handle characters after explicit decoding. Buffering improves efficiency, but it does not by itself guarantee durability. NIO.2 adds portable path and file-system abstractions, attributes, traversal, channels, buffers, and asynchronous operations. These APIs require careful management of resource ownership, partial operations, buffer state, and provider-specific behavior.

Serialization is a representation and compatibility problem as well as a programming task. JSON, XML, delimited text, and binary formats have different strengths and risks. Native Java serialization is unsuitable for untrusted input and fragile for long-term storage. Robust applications locate and authorize resources, decode with explicit policies, validate both syntax and meaning, process data within limits, commit safely, and record appropriate operational evidence. Security, privacy, governance, and evolving deployment environments must influence I/O design from the beginning.

Key Terms

Byte stream: A sequential abstraction that transfers raw bytes.

Character stream: An abstraction that transfers decoded characters through readers or writers.

Charset: A defined mapping between characters and byte sequences.

Buffer: A memory region used to hold data during transfer or processing.

Channel: An NIO abstraction supporting data transfer and, in some cases, positioning and asynchronous operations.

Path: A provider-independent representation of a file-system location.

NIO.2: Java’s modern file-system and I/O API centered on `Path` and `Files`.

Try-with-resources: A language construct that automatically closes declared resources.

File attribute: Metadata describing properties such as size, type, timestamps, ownership, or permissions.

Symbolic link: A file-system object that refers to another path.

Serialization: Conversion of an in-memory representation into a storable or transferable form.

Deserialization: Reconstruction of data or objects from a serialized representation.

Schema: A formal description of the structure, types, and constraints of data.

Schema evolution: Controlled change to a data format while preserving agreed compatibility.

Atomic move: A move operation intended to make a new file visible as one indivisible update where supported.

Memory-mapped file: A file region associated with virtual memory for direct-style access.

Asynchronous I/O: I/O whose completion is reported separately from the initiating operation.

Path traversal: Unauthorized access caused by manipulating path components to escape an intended directory.

Back-pressure: Regulation that prevents producers from overwhelming consumers or available resources.

Review Questions

Short Answer Questions

1. Why should a Java application specify a charset when reading or writing text?

2. What are the roles of `position`, `limit`, and `capacity` in a `ByteBuffer`?

3. Why is try-with-resources important in file processing?

4. Distinguish a byte stream from a character stream.

5. State two reasons native Java serialization is unsuitable for untrusted input.

Long Answer Questions

1. Explain how NIO.2 improves file-system programming compared with basic file stream operations. Include paths, attributes, traversal, and exception handling.

2. Compare streams, channels, memory-mapped files, and asynchronous file channels for different workloads.

3. Discuss the design of a reliable serialization boundary, including schema evolution, validation, compatibility, and security.

4. Explain how buffering works and distinguish performance buffering from durability guarantees.

5. Describe a secure workflow for accepting, validating, storing, and serving user-supplied documents.

Analytical / Application Questions

1. A service reads a user-supplied filename, appends it to `/srv/imports`, and writes the uploaded content with `readAllBytes()`. Identify the principal risks and propose a redesigned workflow.

2. A batch program works correctly on a developer’s computer but corrupts accented text after deployment to another operating system. Analyze the likely cause and recommend corrective measures.

3. An organization must preserve records for ten years while allowing independent systems to read them. Evaluate whether native Java serialization, JSON, XML, or a schema-based binary format is most appropriate, and justify the decision criteria.

22. Multiple Choice Questions with Answers

Question 1

Which choice best explains why an explicit charset should be used when reading a text file?

A. It guarantees that the file is compressed.

B. It ensures that bytes are decoded according to a defined character mapping.

C. It prevents all malformed input.

D. It makes the file system transactionally consistent.

Correct Answer: B. It ensures that bytes are decoded according to a defined character mapping.

Question 2

A program calls channel.read(buffer), and the buffer contains newly read bytes. Which operation normally prepares those bytes for reading from the buffer?

A. clear()

B. mark()

C. flip()

D. rewind() before the read

Correct Answer: C. flip()

Question 3

Which approach is most appropriate for processing an uncontrolled, very large text file?

A. Use readString() without a size limit.

B. Load all bytes into a single byte array.

C. Process it incrementally with a buffered reader or streaming parser.

D. Convert it to a Java serialized object first.

Correct Answer: C. Process it incrementally with a buffered reader or streaming parser.

Question 4

What is the principal security concern when a user-controlled filename is concatenated with an application directory?

A. Character streams become byte streams.

B. The application may be vulnerable to path traversal.

C. The file will always be read asynchronously.

D. The JVM cannot close the file.

Correct Answer: B. The application may be vulnerable to path traversal.

Question 5

Which statement about try-with-resources is correct?

A. It closes declared resources when the protected block finishes, including after an exception.

B. It prevents all I/O exceptions from occurring.

C. It is usable only with files, not channels or streams.

D. It forces every resource to remain open until program termination.

Correct Answer: A. It closes declared resources when the protected block finishes, including after an exception.

Question 6

Why is native Java serialization generally inappropriate for untrusted input?

A. It cannot represent any object state.

B. It always produces human-readable JSON.

C. Deserialization can expose security and compatibility risks.

D. It requires every file to use UTF-8.

Correct Answer: C. Deserialization can expose security and compatibility risks.

Question 7

A system must replace an important configuration file while minimizing the chance that readers observe partial content. Which strategy is strongest?

A. Write directly to the existing file from multiple threads.

B. Write a temporary file and move it into place atomically where supported.

C. Append the new configuration to the old file.

D. Use the platform default charset and ignore close failures.

Correct Answer: B. Write a temporary file and move it into place atomically where supported.

Question 8

Which statement best distinguishes buffering from durability?

A. Buffering reduces transfer overhead, while durability concerns persistence beyond caches.

B. Buffering encrypts data, while durability compresses it.

C. Buffering validates schemas, while durability parses XML.

D. They are identical properties of every output stream.

Correct Answer: A. Buffering reduces transfer overhead, while durability concerns persistence beyond caches.

Question 9

Why can a naïve split(",") operation be unsuitable for CSV data?

A. CSV cannot contain characters.

B. CSV may contain quoted separators, escaped quotes, or embedded line breaks.

C. CSV is always binary.

D. The operation automatically follows symbolic links.

Correct Answer: B. CSV may contain quoted separators, escaped quotes, or embedded line breaks.

Question 10

Which practice best supports long-term interoperability of stored records?

A. Persist private implementation details of Java classes.

B. Rely on the default locale and charset.

C. Define a schema and document compatibility and evolution rules.

D. Permit every reader to interpret fields differently.

Correct Answer: C. Define a schema and document compatibility and evolution rules.

Programming in Java
Chapter 6 of 15